Haskellでは、Tree
を定義できます。
data Tree a = Empty | Node a (Tree a) (Tree a)
どうすればこれをScalaで書くことができますか?
タイプパラメータを保持する方法がわかりません[A]
in Scala for Node
to match Tree
's type、a
。
ADTの定義
Scalaの「オブジェクト機能」モデルでは、ADTとそのすべてのパラメーターを表すtrait
を定義します。次に、ケースごとに、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 class
(Empty
など)をcase object
に置き換えるのが一般的ですが、この場合、case object
はシングルトンであるため、少し注意が必要ですが、Empty
すべてのタイプのツリー。
幸いなことに(または、誰に尋ねるかによっては)、共分散アノテーションを使用すると、1つのcase object Empty
をタイプTree
の空のNothing
として機能させることができます。これは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で動作する機能を整理する方法から選択できる、途方もない一連のオプションを開発者に提供するのが特徴です。私は4つの基本的なアプローチを考えることができます。
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