Scala中的forall

50

如下所示,在Haskell中,可以在列表中存储具有不同类型的值,并对它们施加某些上下文限制:

data ShowBox = forall s. Show s => ShowBox s

heteroList :: [ShowBox]
heteroList = [ShowBox (), ShowBox 5, ShowBox True]

我该如何在Scala中实现相同的功能,最好不使用子类型?


我对Scala了解不够,但我并不100%确定它是否存在存在量化类型。也许可以尝试在谷歌上搜索一下相关信息。 - Michael Kohl
@Michael Kohl:它们可以在Scala中被模拟 - missingfaktor
另外,在我的谷歌搜索中,我找到了这个,但我不知道如何使用它。(实际上,我不知道它是否与问题相关。) - missingfaktor
1
严格来说,这是GHC扩展,而不是纯标准化的Haskell - http://en.wikibooks.org/wiki/Haskell/Existentially_quantified_types。 - shabunc
5个回答

63

正如@Michael Kohl所评论的那样,在Haskell中使用forall是一种存在类型,并且可以使用forSome构造或通配符在Scala中完全复制。这意味着@paradigmatic的答案在很大程度上是正确的。

然而,相对于Haskell原始代码,这里缺少了一些内容,即它的ShowBox类型的实例也以一种方式捕获了相应的Show类型类实例,使它们可以在列表元素上使用,即使精确的基础类型已经被量化出来。您在@paradigmatic答案的评论中表明希望能够编写与以下Haskell等效的内容:

data ShowBox = forall s. Show s => ShowBox s

heteroList :: [ShowBox]
heteroList = [ShowBox (), ShowBox 5, ShowBox True]

useShowBox :: ShowBox -> String
useShowBox (ShowBox s) = show s

-- Then in ghci ...

*Main> map useShowBox heteroList
["()","5","True"]

@Kim Stebel的回答展示了利用子类型在面向对象语言中实现该功能的规范方式。其他条件相等的情况下,这是在Scala中正确的方法。我相信您知道这一点,并且有想要避免子类型并在Scala中复制Haskell的基于类型类的方法的好理由。下面是具体操作...
请注意,在上述Haskell中,Unit、Int和Bool的Show类型类实例可在useShowBox函数的实现中使用。如果我们尝试将其直接翻译成Scala,将得到以下结果:
trait Show[T] { def show(t : T) : String }

// Show instance for Unit
implicit object ShowUnit extends Show[Unit] {
  def show(u : Unit) : String = u.toString
}

// Show instance for Int
implicit object ShowInt extends Show[Int] {
  def show(i : Int) : String = i.toString
}

// Show instance for Boolean
implicit object ShowBoolean extends Show[Boolean] {
  def show(b : Boolean) : String = b.toString
}

case class ShowBox[T: Show](t:T)

def useShowBox[T](sb : ShowBox[T]) = sb match {
  case ShowBox(t) => implicitly[Show[T]].show(t)
  // error here      ^^^^^^^^^^^^^^^^^^^
} 

val heteroList: List[ShowBox[_]] = List(ShowBox(()), ShowBox(5), ShowBox(true))

heteroList map useShowBox

以下是在 useShowBox 中编译失败的内容:

<console>:14: error: could not find implicit value for parameter e: Show[T]
         case ShowBox(t) => implicitly[Show[T]].show(t)
                                      ^

这里的问题在于,与Haskell情况不同的是,Show类型类实例不能从ShowBox参数传播到useShowBox函数的主体部分,因此无法使用。如果我们尝试通过在useShowBox函数上添加额外的上下文限定来解决这个问题,
def useShowBox[T : Show](sb : ShowBox[T]) = sb match {
  case ShowBox(t) => implicitly[Show[T]].show(t) // Now compiles ...
} 

这解决了useShowBox中的问题,但我们现在不能将其与存在量化的List一起使用map。

scala> heteroList map useShowBox
<console>:21: error: could not find implicit value for evidence parameter
                     of type Show[T]
              heteroList map useShowBox
                             ^

这是因为当useShowBox作为map函数的参数时,我们必须根据此时的类型信息选择一个Show实例。显然,并非所有列表元素都可以使用同一种Show实例进行处理,因此编译会失败(如果我们为Any定义了一个Show实例,则所有元素都可以使用该实例,但这并不是我们想要的...我们希望根据每个列表元素的最具体类型来选择一个类型类实例)。
为了使它像Haskell中那样工作,我们必须在useShowBox的主体内明确传播Show实例。可能会像这样:
case class ShowBox[T](t:T)(implicit val showInst : Show[T])

val heteroList: List[ShowBox[_]] = List(ShowBox(()), ShowBox(5), ShowBox(true))

def useShowBox(sb : ShowBox[_]) = sb match {
  case sb@ShowBox(t) => sb.showInst.show(t)
}

然后在 REPL 中执行以下操作:

scala> heteroList map useShowBox
res7: List[String] = List((), 5, true)

请注意,我们对ShowBox上的上下文限定进行了desugared处理,以便我们为包含值的Show实例提供一个明确的名称(showInst)。然后在useShowBox的主体中,我们可以明确地应用它。还要注意,模式匹配对于确保我们仅在函数主体中打开存在类型一次至关重要。
显然,这比等效的Haskell要冗长得多,我强烈建议在Scala中使用基于子类型的解决方案,除非您有极好的理由不这样做。
编辑
正如评论中指出的那样,上面的Scala ShowBox定义具有可见的类型参数,而Haskell原始版本中不存在该参数。我认为通过使用抽象类型来纠正这个问题是非常有启示性的。
首先,我们将类型参数替换为抽象类型成员,并将构造函数参数替换为抽象val。
trait ShowBox {
  type T
  val t : T
  val showInst : Show[T]
}

我们现在需要添加工厂方法,否则我们将无法使用 case 类所提供的免费功能。
object ShowBox {
  def apply[T0 : Show](t0 : T0) = new ShowBox {
    type T = T0
    val t = t0
    val showInst = implicitly[Show[T]]
  } 
}

现在我们可以在先前使用ShowBox[_]的任何地方使用普通的ShowBox...抽象类型成员现在为我们扮演存在量词的角色。
val heteroList: List[ShowBox] = List(ShowBox(()), ShowBox(5), ShowBox(true))

def useShowBox(sb : ShowBox) = {
  import sb._
  showInst.show(t)
}

heteroList map useShowBox

值得注意的是,在 Scala 引入 forSome 和通配符之前,这正是你表示存在类型的方式。现在,我们在 Scala 中的存在类型与原始 Haskell 中完全相同。我认为这是 Scala 中最忠实的表现方式。

6
问题中的ShowBox没有接受类型参数。存在类型(existential type)的整个意义就是隐藏这个类型参数。 - Apocalisp
有没有一种方式可以将类型类参数化,以便可以使用Box [Show](或更一般地说,Box [TC [_]])代替ShowBox?我的尝试可以在http://stackoverflow.com/a/28142861/247623中看到,但我不确定这是最好的方式。 - Erik Kaplun

25

你提供的 ShowBox 示例涉及到一种 存在类型。我将 ShowBox 数据构造器重命名为 SB,以便与该 类型 区分:

data ShowBox = forall s. Show s => SB s

我们说s是“存在的”,但这里的forall是一个普遍量词,它适用于SB数据构造函数。如果我们要求打开显式forallSB构造函数的类型,这一点会变得更加清晰:

SB :: forall s. Show s => s -> ShowBox

换句话说,一个ShowBox实际上是由三部分构成的:

  1. 类型s
  2. 类型为s的值
  3. Show s的一个实例。

因为类型s成为构造的ShowBox的一部分,所以它是存在量化的。如果Haskell支持存在量化的语法,我们可以将ShowBox写为类型别名:

type ShowBox = exists s. Show s => s

Scala支持这种存在量词,Miles的回答介绍了使用恰好包含上述三个内容的特质。但由于这是关于“Scala中的forall”的问题,让我们像Haskell一样来做。

在Scala中,数据构造函数不能显式地用forall进行量化。但是,模块上的每个方法都可以。因此,您可以有效地使用类型构造函数多态作为普遍量化。例如:

trait Forall[F[_]] {
  def apply[A]: F[A]
}

对于给定的 F,Scala 类型 Forall[F] 相当于 Haskell 类型 forall a. F a

我们可以使用这种技术来为类型参数添加约束。

trait SuchThat[F[_], G[_]] {
  def apply[A:G]: F[A]
}

F SuchThat G 类型的值类似于 Haskell 类型 forall a. G a => F a 的值。如果实例 G[A] 存在,则 Scala 会隐式地查找它。

现在,我们可以使用这个来编码你的 ShowBox...

import scalaz._; import Scalaz._ // to get the Show typeclass and instances

type ShowUnbox[A] = ({type f[S] = S => A})#f SuchThat Show

sealed trait ShowBox {
  def apply[B](f: ShowUnbox[B]): B  
}

object ShowBox {
  def apply[S: Show](s: => S): ShowBox = new ShowBox {
    def apply[B](f: ShowUnbox[B]) = f[S].apply(s)
  }
  def unapply(b: ShowBox): Option[String] =
    b(new ShowUnbox[Option[String]] {
      def apply[S:Show] = s => some(s.shows)
  })
}

val heteroList: List[ShowBox] = List(ShowBox(()), ShowBox(5), ShowBox(true))

ShowBox.apply方法是普适量的数据构造函数。你可以看到它接受一个类型为S,一个Show[S]实例和一个类型为S的值,就像Haskell版本一样。

这里是一个使用示例:

scala> heteroList map { case ShowBox(x) => x }
res6: List[String] = List((), 5, true)

在Scala中更直接的编码方式可能是使用case class:

sealed trait ShowBox
case class SB[S:Show](s: S) extends ShowBox {
  override def toString = Show[S].shows(s)
}

那么:

scala> val heteroList = List(ShowBox(()), ShowBox(5), ShowBox(true))
heteroList: List[ShowBox] = List((), 5, true)
在这种情况下,List[ShowBox] 基本上相当于 List[String],但您可以使用与 Show 不同的 trait 来获得更有趣的东西。 这里所使用的是来自 ScalazShow typeclass。

不错,但你从 Haskell 中的不同 forall 用法开始。原始问题中的那个是存在量化,而不是排名-N 类型。 - Miles Sabin
在原始的“data ShowBox = forall ...”中,forall是一种存在量词。你的回答没有直接反映出来。 - Miles Sabin
scala> type X[A, B] = A Tuple2 B 已定义类型别名X天哪,我以前从未见过二元类型构造函数被这样使用。太棒了。 - Kris Nuttycombe
@Daniel 哦,是的。我想我很少考虑文本名称进行这样的处理;操作符名称更自然。 - Kris Nuttycombe
1
@C. A. McCann,我意识到了。我仍然坚持认为,从与原始Haskell相同的存在性开始的解决方案比从通用性开始并逆转它更直接地编码了问题。最终,在恰当的等价关系下,一切都对一切其他事物同构,所以,如常,你的情况可能有所不同。 - Miles Sabin
显示剩余5条评论

5
我认为在这里不可能进行一对一从Haskell到Scala的翻译。但是,为什么不想使用子类型呢?如果您想要使用的类型(如Int)缺少show方法,您仍然可以通过隐式转换来添加它。
scala> trait Showable { def show:String }
defined trait Showable

scala> implicit def showableInt(i:Int) = new Showable{ def show = i.toString }
showableInt: (i: Int)java.lang.Object with Showable

scala> val l:List[Showable] = 1::Nil
l: List[Showable] = List($anon$1@179c0a7)

scala> l.map(_.show)
res0: List[String] = List(1)

假设我有各种数据类型,它们唯一的共同点是它们满足某些上下文限制。我想获取这些数据类型值的集合,并将它们应用于符合它们上下文限制的函数。 - missingfaktor
你不能这样做,因为上下文边界不是类型的一部分。 - Kim Stebel
1
它们在Haskell中也不是。然而,rank-2类型提供了一种以多态方式处理它们的方法。我在想Scala中是否也可能。 - missingfaktor

3
编辑:添加了显示方法和回答评论的方法。)
我认为你可以使用上下文界定和隐式方法获得相同的结果:
trait Show[T] {
  def apply(t:T): String
}
implicit object ShowInt extends Show[Int] {
  def apply(t:Int) = "Int("+t+")"
}
implicit object ShowBoolean extends Show[Boolean] {
  def apply(t:Boolean) = "Boolean("+t+")"
}

case class ShowBox[T: Show](t:T) {
  def show = implicitly[Show[T]].apply(t)
}

implicit def box[T: Show]( t: T ) =
  new ShowBox(t)

val lst: List[ShowBox[_]] = List( 2, true )

println( lst ) // => List(ShowBox(2), ShowBox(true))

val lst2 = lst.map( _.show )

println( lst2 ) // => List(Int(2), Boolean(true))

不完全正确。在Haskell的情况下,我可以根据列表中的约束条件对元素应用函数。但是在Scala中(采用你的方法),我无法做到这一点。 - missingfaktor

0

为什么不呢:

trait ShowBox {
    def show: String
}

object ShowBox {
    def apply[s](x: s)(implicit i: Show[s]): ShowBox = new ShowBox {
        override def show: String = i.show(x)
    }
}

根据官方的回答,我经常感到惊讶的是,Scala可以将“Haskell类型怪物”转化为非常简单的类型。

这与我发布的代码片段不同构。实际上,它甚至都不接近。 - missingfaktor
在将parsec3移植到Scala时,我可能理解了你的问题。 - MB_

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接