簡體   English   中英

在 Scala 中編寫代數數據類型

[英]Writing Algebraic Data Type in Scala

在 Haskell 中,我可以定義一個Tree

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

我怎么能用 Scala 寫這個?

我不確定如何在 Scala 中為Node保留類型參數[A]以匹配Tree的類型a

定義 ADT

在Scala的“目標函數”的模式,您可以定義一個trait表示ADT和它所有的參數。 然后對於您的每個案例,您定義一個case class或一個case 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)您可以使其成為特征外部的獨立函數:

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 實例,這可能會很好。 伴隨對象通常是放置此類方法的自然場所。

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)
  }
}

這是一種特別方便的方法,可以增強您無法控制的代碼中定義的類型,將輔助行為從您的類型中分解出來,以便它們可以專注於核心行為,或者促進臨時多態性

方法 1 是一個采用Tree的函數,其工作方式與第一個示例類似。 方法 2-4 都是對Tree操作:

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

Scala 3和新的聯合類型開始,這將成為可能:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM