Написание алгебраического типа данных в Scala

В Haskell я могу определить Tree:

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

Как я мог написать это на Scala?

Я не уверен, как сохранить параметр типа [A] в Scala для Node, чтобы он соответствовал типу Tree, a.


person Kevin Meredith    schedule 01.11.2014    source источник
comment
Закрытые классы case, параметрические на A. docs.scala-lang.org/ tutorials/tour/case-classes.html   -  person chi    schedule 01.11.2014
comment
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.   -  person Kevin Meredith    schedule 01.11.2014
comment
Я написал небольшой обзор о scala Enumeration и альтернативах, он может оказаться полезным: pedrorijo.com/blog/scala-enums/   -  person pedrorijo91    schedule 08.06.2017


Ответы (2)


Определение ADT

В «объектно-функциональной» модели Scala вы определяете 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 для каждого типа дерева.

К счастью (или нет, в зависимости от того, кого вы спросите), с ковариационной аннотацией вы можете иметь одно действие case object Empty в качестве пустого Tree типа Nothing, который является универсальным подтипом Scala. Из-за ковариации этот Empty теперь является подтипом Tree[A] для всех возможных 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, как правило, предоставляет разработчику ошеломляющий набор возможностей для выбора в том, как организовать функциональность, работающую с АТД. Я могу назвать четыре основных подхода.

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
person acjay    schedule 01.11.2014

Начиная с 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
person Xavier Guihot    schedule 01.06.2020