【问题标题】:Writing Algebraic Data Type in Scala在 Scala 中编写代数数据类型
【发布时间】:2014-12-28 16:12:38
【问题描述】:

在 Haskell 中,我可以定义一个Tree

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

我怎么能用 Scala 写这个?

我不确定如何在 Scala 中为 Node 保留类型参数 [A] 以匹配 Tree 的类型 a

【问题讨论】:

  • 密封案例类,A 上的参数化。docs.scala-lang.org/tutorials/tour/case-classes.html
  • case classes 本质上是密封的,我相信:scala> case class Bar(x: Int) extends Foo() <console>:9: error: case class Bar has case ancestor Foo, but case-to-case inheritance is prohibited.
  • 我写了一个关于 scala 枚举和替代方案的小概述,你可能会发现它很有用:pedrorijo.com/blog/scala-enums/

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


【解决方案1】:

Scala 3 开始,以及新的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

【讨论】:

    【解决方案2】:

    定义 ADT

    在 Scala 的“对象功能”模型中,您定义了一个 trait 来表示 ADT 及其所有参数。然后对于您的每个案例,您可以定义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 object 替换零参数 case class,例如 Empty,虽然在这种情况下,这有点棘手,因为 case object 是单例,但我们需要一个Empty 适用于每种类型的树。

    幸运的是(或者不是,取决于你问谁),通过协方差注释,你可以让一个case object Empty 充当Nothing 类型的空Tree,这是Scala 的通用子类型。由于协方差,这个Empty 现在是所有可能的ATree[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)
    

    事实上,这就是 Scala 的标准库 Nil 相对于 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) 你可以让它成为 trait 外部的独立函数:

    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) 你可以让它成为 trait 本身的具体方法:

    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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-01-01
      • 2023-04-01
      • 2020-02-02
      • 1970-01-01
      • 2011-03-03
      相关资源
      最近更新 更多