[英]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
現在是所有可能A
的Tree[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.