web-dev-qa-db-ja.com

代数的データ型をScala

Haskellでは、Treeを定義できます。

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

どうすればこれをScalaで書くことができますか?

タイプパラメータを保持する方法がわかりません[A] in Scala for Node to match Tree's type、a

36
Kevin Meredith

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 classEmptyなど)をcase objectに置き換えるのが一般的ですが、この場合、case objectはシングルトンであるため、少し注意が必要ですが、Emptyすべてのタイプのツリー。

幸いなことに(または、誰に尋ねるかによっては)、共分散アノテーションを使用すると、1つのcase object EmptyをタイプTreeの空のNothingとして機能させることができます。これは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で動作する機能を整理する方法から選択できる、途方もない一連のオプションを開発者に提供するのが特徴です。私は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
77
acjay