【问题标题】:forall in ScalaScala中的forall
【发布时间】:2011-11-05 00:49:15
【问题描述】:

如下所示,在 Haskell 中,可以在列表中存储具有特定上下文边界的异构类型的值:

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

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

如何在 Scala 中实现相同的目标,最好不使用子类型?

【问题讨论】:

标签: scala haskell forall


【解决方案1】:

我认为从 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)

【讨论】:

  • 假设我有各种数据类型,其中唯一的共同点是它们满足某些上下文界限。我想收集这些数据类型的值,并将它们应用到它们的上下文边界允许的函数中。
  • 你不能这样做,因为上下文边界不是类型的一部分。
  • 它们也不在 Haskell 中。然而,rank-2 类型提供了多态处理它们的方法。我想知道在 Scala 中是否也可以这样做。
【解决方案2】:

您给出的ShowBox 示例涉及一个存在类型。我将 ShowBox 数据构造函数重命名为 SB 以将其与 type 区分开来:

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

我们说s 是“存在的”,但这里的forall 是属于SB 数据构造函数的通用量词。如果我们在显式 forall 开启的情况下询问 SB 构造函数的类型,这将变得更加清晰:

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

也就是说,ShowBox 实际上是由三件事构成的:

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

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

type ShowBox = exists s. Show s => s

Scala 确实支持这种存在量化,并且 Miles 的回答使用由上述三件事组成的特征给出了详细信息。但既然这是一个关于“forall in Scala”的问题,让我们像 Haskell 那样做吧。

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

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

Scala 类型Forall[F],给定一些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 中更直接的编码可能是使用案例类:

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 以外的特征一起使用以获得更有趣的东西。

这都是使用来自ScalazShow 类型类。

【讨论】:

  • 很好,但您是从 Haskell 中 forall 的不同用法开始的。原始问题中的一个是存在量化,而不是 rank-n 类型。
  • 废话。这里使用的 forall 是消除器类型中的通用量词。就整体数据类型而言,它是存在的,但这在这里并不重要。当谈到它的 Church 编码时,它实际上只是一个通用量词,Scala 以一种直接的方式支持它。
  • 在原始的“data ShowBox = forall ...”中,forall 是一个存在量词。您没有在回答中直接反映这一点。
  • 是的,我有。事实上,我已经以与 Haskell 编码存在的完全相同的方式完成它:hackage.haskell.org/trac/haskell-prime/wiki/…
  • @C. A. McCann,我知道这一点。我仍然坚持认为,从与原始 Haskell 中存在的相同存在开始的解决方案是对问题的更直接的编码,而不是从普遍开始并反转到问题的解决方案。最后,在正确的等价关系下,一切都与其他一切同构,因此,一如既往,YMMV。
【解决方案3】:

为什么不:

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 类型的怪物”翻译成非常简单的怪物感到惊讶。

【讨论】:

  • 这与我发布的代码 sn-p 不同。实际上它甚至没有接近。
  • 在将 parsec3 移植到 Scala 时,我可能理解您的问题。
【解决方案4】:

正如@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 上绑定的上下文进行了脱糖处理,以便我们为包含的值的 Show 实例提供一个明确的名称 (showInst)。然后在 useShowBox 的主体中我们可以显式地应用它。另请注意,模式匹配对于确保我们只在函数体中打开存在类型一次是必不可少的。

显然,这比等效的 Haskell 更加麻烦,我强烈建议您在 Scala 中使用基于子类型的解决方案,除非您有充分的理由不这样做。

编辑

正如 cmets 中所指出的,上面 ShowBox 的 Scala 定义有一个可见的类型参数,这在 Haskell 原版中是不存在的。我认为看看我们如何使用抽象类型来纠正这一点实际上很有启发性。

首先我们将类型参数替换为抽象类型成员,并将构造函数参数替换为抽象 val,

trait ShowBox {
  type T
  val t : T
  val showInst : Show[T]
}

我们现在需要添加工厂方法,否则案例类会免费提供给我们,

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 和通配符之前,这正是您表示存在类型的方式。)

我们现在在与原来的 Haskell 完全相同的地方拥有存在主义。我认为这是您在 Scala 中可以得到的最接近忠实的演绎。

【讨论】:

  • 问题中的 ShowBox 不带类型参数。存在类型的全部意义在于隐藏这个类型参数。
  • 有没有办法对类型类进行参数化,以便您可以使用Box[Show](或更一般地说,Box[TC[_]])来代替ShowBox?我的尝试可以在stackoverflow.com/a/28142861/247623 看到,但我不相信这是最好的方法。
【解决方案5】:

编辑:添加方法来展示,回答评论。)

我认为您可以使用带有上下文边界的隐式方法获得相同的效果:

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 中做同样的事情(就是用你的方法)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-08-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多