在Scala中编写代数数据类型

时间:2014-11-01 13:51:26

标签: scala haskell type-parameter abstract-data-type

在Haskell中,我可以定义Tree

data Tree a = Empty | Node a (Tree a) (Tree a)

我怎么能在Scala中写这个?

我不确定如何在Scala中保留类型参数[A]以使NodeTree的类型a匹配。< / p>

2 个答案:

答案 0 :(得分:74)

定义ADT

在Scala&#34;对象功能&#34;在模型中,您定义一个代表ADT及其所有参数的trait。然后,对于每种情况,您都可以定义case classcase object。类型和值参数被视为类构造函数的参数。通常,您创建特征sealed,以便当前文件之外的任何内容都不能添加案例。

sealed trait Tree[A]
case class Empty[A]() extends Tree[A]
case class Node[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A]

然后你可以这样做:

scala> Node("foo", Node("bar", Empty(), Empty()), Empty())
res2: Node[String] = Node(foo,Node(bar,Empty(),Empty()),Empty())

当该类没有数据时,我们必须创建一大堆新的Empty实例,这有点令人讨厌。在Scala中,通常的做法是使用case class替换零Empty,如case object,尽管在这种情况下,它有点棘手,因为case object是一个单身人士,但我们需要为每种类型的树提供Empty

幸运(或不是,取决于你问的对象),使用协方差注释,你可以让一个case object Empty充当Tree类型的空Nothing,这是Scala&# 39; s通用子类型。由于协方差,此Empty现在是所有可能的Tree[A] A的子类型:

sealed trait Tree[+A]
case object Empty extends Tree[Nothing]
case class Node[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A]

然后你会得到更清晰的语法:

scala> Node("foo", Node("bar", Empty, Empty), Empty)
res4: Node[String] = Node(foo,Node(bar,Empty,Empty),Empty)

事实上,就Nil而言,Scala的标准库List的工作原理。

在ADT上操作

要使用新的ADT,Scala中常见的是定义使用match关键字解构它的递归函数。参见:

scala> :paste
// Entering paste mode (ctrl-D to finish)

import scala.math.max
def depth[A](tree: Tree[A]): Int = tree match {
  case Empty => 0
  case Node(_, left, right) => 1 + max(depth(left), depth(right))
}

// Exiting paste mode, now interpreting.

import scala.math.max
depth: [A](tree: Tree[A])Int

scala> depth(Node("foo", Node("bar", Empty, Empty), Empty))
res5: Int = 2

Scala特征性地为开发人员提供了一系列令人眼花缭乱的选项,可供选择如何组织在ADT上运行的功能。我可以想到四种基本方法。

1)您可以将其作为特征外部的独立功能:

sealed trait Tree[+A]
case object Empty extends Tree[Nothing]
case class Node[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A]

object Tree {
  def depth[A](tree: Tree[A]): Int = tree match {
    case Empty => 0
    case Node(_, left, right) => 1 + max(depth(left), depth(right))
  }
}

如果您希望API比面向对象更有用,或者您的操作可能从其他数据生成ADT实例,那么这可能会很好。 companion object通常是放置此类方法的自然场所。

2)你可以把它作为特质的具体方法:

sealed trait Tree[+A] {
  def depth: Int = this match {
    case Empty => 0
    case Node(_, left, right) => 1 + max(left.depth, right.depth)
  }
}
case object Empty extends Tree[Nothing]
case class Node[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A]

如果您的操作纯粹根据trait的其他方法定义,则此功能特别有用,在这种情况下,您可能无法明确使用match

3)您可以使用子类型中的具体实现使其成为特征的抽象方法(避免使用match):

sealed trait Tree[+A] {
  def depth: Int
}
case object Empty extends Tree[Nothing] {
  val depth = 0
} 
case class Node[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A] {
  def depth = 1 + max(left.depth, right.depth)
}

这与传统的面向对象多态的方法最为相似。在定义trait的低级操作时,我感觉很自然,在trait本身中根据这些操作定义了更丰富的功能。在处理不是sealed的特征时,这也是最合适的。

4)或者,如果要将方法添加到源项在项目外部的ADT,可以使用隐式转换为具有以下方法的新类型:

// assuming Tree defined elsewhere
implicit class TreeWithDepth[A](tree: Tree[A]) {
  def depth: Int = tree match {
    case Empty => 0
    case Node(_, left, right) => 1 + max(left.depth, right.depth)
  }
}

这是一种特别方便的方法,可以增强您无法控制的代码中定义的类型,将辅助行为从您的类型中分解出来,以便他们可以专注于核心行为,或促进ad hoc polymorphism

方法1是一个采用Tree的函数,其工作方式与第一个示例类似。方法2-4是Tree上的所有操作:

scala> Node("foo", Node("bar", Empty, Empty), Empty).depth
res8: Int = 2

答案 1 :(得分:0)

随着即将到来的Scala 3Dotty)和新的union类型的出现,这将成为可能:

type Tree[A] = Node[A] | Empty.type
case object Empty
case class Node[A](value: A, left: Tree[A], right: Tree[A])

您可以这样实例化:

val empty: Tree[String] = Empty
val tree:  Tree[String] = Node("foo", Node("bar", Empty, Empty), Empty)

并用作具体示例的一部分:

def depth[A](tree: Tree[A]): Int =
  tree match {
    case Empty                => 0
    case Node(_, left, right) => 1 + (depth(left) max depth(right))
  }

depth(tree)  // 2
depth(empty) // 0