在Scala中,如何定义在运行时工作的联合类型?

时间:2016-07-10 20:02:50

标签: scala functional-programming

继续形成关于如何定义union types in Scala的这一优秀答案。我一直在使用Miles Sabin对联盟类型的定义,但仍有一个问题。

如果直到运行时才知道类型,您如何使用这些?例如:

trait inv[-A] {}
type Or[A,B] = {
  type check[X] = (inv[A] with inv[B]) <:< inv[X]
}

case class Foo[A : (Int Or String)#check](a: A)

Foo(1)    // Foo[Int] = Foo(1)
Foo("hi") // Foo[String] = Foo(hi)
Foo(2.0)  // Error!

此示例有效,因为参数A在编译时已知,调用Foo(1)实际上正在调用Foo[Int](1)。但是,如果参数A在运行时才知道,您会怎么做?也许您正在削减包含Foo数据的文件,在这种情况下,Foo的类型参数在您读取数据之前不会知道。在这种情况下,没有简单的方法来设置参数A

我能够提出的最佳解决方案是:

  • 模式匹配您已阅读的数据,然后根据该类型创建不同的Foo。在我的情况下,这是不可行的,因为我的case-class实际上包含了几十个union类型,所以有几百种类型的模式匹配组合。

  • 将您刚刚阅读的类型转换为(String or Int),因此您只需要传递一种类型,在使用它创建Foo时会传递类型类约束。然后返回Foo[_]。这使得Foo用户有责任计算出每个字段的类型(因为它们似乎是Any类型),但至少在实际使用该字段之前它必须知道类型,其中案例模式匹配似乎更容易处理。

第二个解决方案如下:

def parseLine: Any // Parses data point, but can be either a String or 
                   // Int, so returns Any.

def mkFoo: Foo[_] = {
  val a = parseLine.asInstanceOf[Int with String]
  Foo(a) // Passes type constraint now
}

在实践中,我最终使用了第二种解决方案,但我想知道我能做些什么更好的事情?

说明问题的另一种方法是:返回联盟类型是什么意思?函数只能返回一个类型,我们与Miles Sabin联合类型一起使用的技巧仅对您传入的类型有用,而不适用于您返回的类型。

PS。对于上下文,为什么在我的情况下这是一个问题是我从Json模式文件生成一组case-classes。 Json自然支持联合类型,所以我想让我的案例类也反映出来。这在一个方向上很有用:用户创建要序列化到Json的case-classes。但是在另一个方向变得棘手:用户解析Json文件以返回一组填充的case类。

3 个答案:

答案 0 :(得分:8)

&#34;标准&#34; Scala解决这个问题的方法是使用普通的区分联合类型(即,完全放弃真正的联合类型):

sealed trait Foo
case class IntFoo(x: Int) extends Foo
case class StringFoo(x: String) extends Foo

这反映了这样一个事实,正如您所观察到的,成员的特定类型是运行时值; Foo实例的JVM类型标记提供此运行时值。

Miles Sabin的联盟类型的实现非常聪明,但我不确定它是否提供任何实际好处,因为它只限制可以进入Foo的事物的类型,但Foo的用户没有该限制的可计算版本,就像match为您提供sealed特征的可计算版本的方式。一般来说,为了使限制变得有用,它需要两个方面:检查只有正确的东西被放入,以及提取器(也称为消除器)允许相同的正确的东西从另一端出来。

也许如果你对为什么要寻找更纯粹的联合类型给出一些解释,那么它将说明常规的歧视联盟是否足够,或者你是否真的需要更多的东西。

答案 1 :(得分:2)

有一个原因,Scala的每个JSON解析器都需要定义好JSON的类型,即使必须删除某些字段:你不能使用你不知道的类型

举个例子,假设你有aString可能是Int,也许它只是a,但你不会#39} ;知道它是什么。为什么用a进行计算,而不知道它的类型?为什么你的代码会计算所有a的总和,例如,如果你事先并不知道它是一个数字?

通常,答案是在运行时对具有未知特征的数据执行用户提供的数据操作,因为用户自己看到它是一个数字并决定他们想要知道该字段的总和是什么。很好,但是如果是这样的话你会走错路。

一种定义良好的方式来表示Scala中的JSON数据(就此而言,任何与JSON具有相同特征的数据。这是使用类的层次结构。一个json值可以是json对象,数组或多个基元之一.json对象包含键/值对的列表,其键是json字符串,值是json值。依此类推。这很容易表示,并且很多图书馆已经这样做了。实际上,有很多项目名为Json4s,它提供了一个统一的API,可以使用并由许多上述库实现。

Miles Sabin Shapeless库提供的记录之类的内容旨在当输入没有明确定义的架构时使用,但程序从输入中知道它需要什么。并且,是的,如果IntString或{{1}},程序可能知道如何处理{{1}},而不是每个可能的值。

答案 2 :(得分:1)

基于Dotty的下一个Scala 3(2020年中)将从2018年9月开始实施proposal for Union Type

您会在“ a tour of Scala 3”中看到它(2019年6月)

  
      
  • 联合类型提供类型的临时组合
  •   
  • 子集=子类型
  •   
  • 没有装箱费
  •   
case class UserName(name: String) 
case class Password(hash: Hash) 

def help(id: UserName | Password) = {
  val user = id match { 
    case UserName(name) => lookupName(name) 
    case Password(hash) => lookupPassword(hash) 
  } 
  ... 
}
  
      
  • 联合类型也适用于单例类型
  •   
  • 非常适合JS互操作
  •   
type Command = "Click" | "Drag" | "KeyPressed" 

def handleEvent(kind: Command) = kind match {
  case "Click" => MouseClick() 
  case "Drag" => MoveTo() 
  case "KeyPressed" => KeyPressed() 
} 
相关问题