如何在事先不知道类型的情况下动态构造 Scala class?
How can I dynamically construct a Scala class without knowing types in advance?
我想构建一个简单的库,开发人员可以在其中定义表示命令行参数的 Scala class(为简单起见,只有一组必需的参数——没有标志或可选参数).我希望库能够解析命令行参数和 return class 的一个实例。图书馆的用户只会做这样的事情:
case class FooArgs(fluxType: String, capacitorCount: Int)
def main(args: Array[String]) {
val argsObject: FooArgs = ArgParser.parse(args).as[FooArgs]
// do real stuff
}
如果提供的参数与预期类型不匹配(例如,如果有人在预期 Int
的位置传递字符串 "bar"),解析器应抛出运行时错误。
如何在事先不知道其形状的情况下动态构建 FooArgs
?由于 FooArgs
可以有任何数量或类型,我不知道如何遍历命令行参数,将它们强制转换或转换为预期的类型,然后使用结果构建 FooArgs
。基本上,我想按照这些思路做一些事情:
// ** notional code - does not compile **
def parse[T](args: Seq[String], klass: Class[T]): T = {
val expectedTypes = klass.getDeclaredFields.map(_.getGenericType)
val typedArgs = args.zip(expectedTypes).map({
case (arg, String) => arg
case (arg, Int) => arg.toInt
case (arg, unknownType) =>
throw new RuntimeException(s"Unsupported type $unknownType")
})
(klass.getConstructor(typedArgs).newInstance _).tupled(typedArgs)
}
关于如何实现这样的目标有什么建议吗?
当你想抽象超过 case class
(或 Tuple
)形状时,标准方法是借助 case class
获得 HList
表示shapeless
图书馆。 HList
在其类型签名中跟踪其元素类型及其数量。然后就可以在HList
上递归实现自己想要的算法了。 Shapeless 还为 shapeless.ops.hlist
.
中的 HList
提供了一些有用的转换
对于这个问题,首先我们需要定义一个辅助类型类来解析来自String
:
的某种类型的参数
trait Read[T] {
def apply(str: String): T
}
object Read {
def make[T](f: String => T): Read[T] = new Read[T] {
def apply(str: String) = f(str)
}
implicit val string: Read[String] = make(identity)
implicit val int: Read[Int] = make(_.toInt)
}
如果您需要支持除 String
或 Int
.
以外的其他参数类型,您可以定义此类型类的更多实例
然后我们可以定义将一系列参数解析为某种类型的实际类型类:
// This is needed, because there seems to be a conflict between
// HList's :: and the standard Scala's ::
import shapeless.{:: => :::, _}
trait ParseArgs[T] {
def apply(args: List[String]): T
}
object ParseArgs {
// Base of the recursion on HList
implicit val hnil: ParseArgs[HNil] = new ParseArgs[HNil] {
def apply(args: List[String]) =
if (args.nonEmpty) sys.error("too many args")
else HNil
}
// A single recursion step on HList
implicit def hlist[T, H <: HList](
implicit read: Read[T], parseRest: ParseArgs[H]
): ParseArgs[T ::: H] = new ParseArgs[T ::: H] {
def apply(args: List[String]) = args match {
case first :: rest => read(first) :: parseRest(rest)
case Nil => sys.error("too few args")
}
}
// The implementation for any case class, based on its HList representation
implicit def caseClass[C, H <: HList](
implicit gen: Generic.Aux[C, H], parse: ParseArgs[H]
): ParseArgs[C] = new ParseArgs[C] {
def apply(args: List[String]) = gen.from(parse(args))
}
}
最后我们可以定义一些使用此类型类的 API。例如:
case class ArgParser(args: List[String]) {
def to[C](implicit parseArgs: ParseArgs[C]): C = parseArgs(args)
}
object ArgParser {
def parse(args: Array[String]): ArgParser = ArgParser(args.toList)
}
还有一个简单的测试:
scala> ArgParser.parse(Array("flux", "10")).to[FooArgs]
res0: FooArgs = FooArgs(flux,10)
有一个关于使用 shapeless
解决类似问题的很好的指南,您可能会发现它有帮助:The Type Astronaut’s Guide to Shapeless
我想构建一个简单的库,开发人员可以在其中定义表示命令行参数的 Scala class(为简单起见,只有一组必需的参数——没有标志或可选参数).我希望库能够解析命令行参数和 return class 的一个实例。图书馆的用户只会做这样的事情:
case class FooArgs(fluxType: String, capacitorCount: Int)
def main(args: Array[String]) {
val argsObject: FooArgs = ArgParser.parse(args).as[FooArgs]
// do real stuff
}
如果提供的参数与预期类型不匹配(例如,如果有人在预期 Int
的位置传递字符串 "bar"),解析器应抛出运行时错误。
如何在事先不知道其形状的情况下动态构建 FooArgs
?由于 FooArgs
可以有任何数量或类型,我不知道如何遍历命令行参数,将它们强制转换或转换为预期的类型,然后使用结果构建 FooArgs
。基本上,我想按照这些思路做一些事情:
// ** notional code - does not compile **
def parse[T](args: Seq[String], klass: Class[T]): T = {
val expectedTypes = klass.getDeclaredFields.map(_.getGenericType)
val typedArgs = args.zip(expectedTypes).map({
case (arg, String) => arg
case (arg, Int) => arg.toInt
case (arg, unknownType) =>
throw new RuntimeException(s"Unsupported type $unknownType")
})
(klass.getConstructor(typedArgs).newInstance _).tupled(typedArgs)
}
关于如何实现这样的目标有什么建议吗?
当你想抽象超过 case class
(或 Tuple
)形状时,标准方法是借助 case class
获得 HList
表示shapeless
图书馆。 HList
在其类型签名中跟踪其元素类型及其数量。然后就可以在HList
上递归实现自己想要的算法了。 Shapeless 还为 shapeless.ops.hlist
.
HList
提供了一些有用的转换
对于这个问题,首先我们需要定义一个辅助类型类来解析来自String
:
trait Read[T] {
def apply(str: String): T
}
object Read {
def make[T](f: String => T): Read[T] = new Read[T] {
def apply(str: String) = f(str)
}
implicit val string: Read[String] = make(identity)
implicit val int: Read[Int] = make(_.toInt)
}
如果您需要支持除 String
或 Int
.
然后我们可以定义将一系列参数解析为某种类型的实际类型类:
// This is needed, because there seems to be a conflict between
// HList's :: and the standard Scala's ::
import shapeless.{:: => :::, _}
trait ParseArgs[T] {
def apply(args: List[String]): T
}
object ParseArgs {
// Base of the recursion on HList
implicit val hnil: ParseArgs[HNil] = new ParseArgs[HNil] {
def apply(args: List[String]) =
if (args.nonEmpty) sys.error("too many args")
else HNil
}
// A single recursion step on HList
implicit def hlist[T, H <: HList](
implicit read: Read[T], parseRest: ParseArgs[H]
): ParseArgs[T ::: H] = new ParseArgs[T ::: H] {
def apply(args: List[String]) = args match {
case first :: rest => read(first) :: parseRest(rest)
case Nil => sys.error("too few args")
}
}
// The implementation for any case class, based on its HList representation
implicit def caseClass[C, H <: HList](
implicit gen: Generic.Aux[C, H], parse: ParseArgs[H]
): ParseArgs[C] = new ParseArgs[C] {
def apply(args: List[String]) = gen.from(parse(args))
}
}
最后我们可以定义一些使用此类型类的 API。例如:
case class ArgParser(args: List[String]) {
def to[C](implicit parseArgs: ParseArgs[C]): C = parseArgs(args)
}
object ArgParser {
def parse(args: Array[String]): ArgParser = ArgParser(args.toList)
}
还有一个简单的测试:
scala> ArgParser.parse(Array("flux", "10")).to[FooArgs]
res0: FooArgs = FooArgs(flux,10)
有一个关于使用 shapeless
解决类似问题的很好的指南,您可能会发现它有帮助:The Type Astronaut’s Guide to Shapeless