АТД (которые в данном контексте являются не абстрактные типы данных, что еще одно понятие, но Алгебраические типы данных) и классы типов - это совершенно разные понятия, которые решают разные проблемы.
ADT, как следует из акронима, является типом данных. Для структурирования ваших данных необходимы ADT. Самое близкое совпадение в Scala, я думаю, представляет собой комбинацию классов дел и запечатанных признаков. Это основное средство построения сложных структур данных в Haskell. Я думаю, что самое известный пример ADT является Maybe
типа:
data Maybe a = Nothing | Just a
Этого типа имеет прямой эквивалент в стандартной библиотеке Scala, называется Option
:
sealed trait Option[+T]
case class Some[T](value: T) extends Option[T]
case object None extends Option[Nothing]
Это не точно, как Option
определяется в стандартной библиотеки, но вы понимаете суть.
В основном ADT представляет собой комбинацию (в некотором смысле) из нескольких названных кортежей (0-арной, так как Nothing
/None
; 1-ичных, так как Just a
/Some(value)
; выше арностей возможны тоже).
Рассмотрим следующий тип данных:
-- Haskell
data Tree a = Leaf | Branch a (Tree a) (Tree a)
// Scala
sealed trait Tree[+T]
case object Leaf extends Tree[Nothing]
case class Branch[T](value: T, left: Tree[T], right: Tree[T]) extends Tree[T]
Это простое бинарное дерево. Оба эти определения в основном выглядят следующим образом: «Бинарное дерево является либо Leaf
, либо Branch
, если оно является ветвью, то оно содержит некоторое значение и два других дерева». Это означает, что если у вас есть переменная типа Tree
, то она может содержать либо Leaf
, либо Branch
, и вы можете проверить, какой из них есть, и при необходимости извлекать содержащиеся данные. Основным средством для таких проверок и экстрагирования сопоставление с образцом:
-- Haskell
showTree :: (Show a) => Tree a -> String
showTree tree = case tree of
Leaf -> "a leaf"
Branch value left right -> "a branch with value " ++ show value ++
", left subtree (" ++ showTree left ++ ")" ++
", right subtree (" ++ showTree right ++ ")"
// Scala
def showTree[T](tree: Tree[T]) = tree match {
case Leaf => "a leaf"
case Branch(value, left, right) => s"a branch with value $value, " +
s"left subtree (${showTree(left)}), " +
s"right subtree (${showTree(right)})"
}
Эта концепция очень проста, но очень мощный.
Как вы заметили, ADT закрыт, то есть вы не можете добавить еще именованные кортежи после того, как тип был определен. В Haskell это выполняется синтаксически, а в Scala это достигается с помощью ключевого слова sealed
, которое запрещает подклассы в других файлах.
Эти типы называются алгебраическими по причине. Именованные кортежи можно рассматривать как продукты (в математическом смысле) и «комбинации» этих кортежей как суммирование (также в математическом смысле), и такое рассмотрение имеет глубокий теоретический смысл. Например, вышеупомянутый двоичный тип дерева может быть записана следующим образом:
Tree a = 1 + a * (Tree a) * (Tree a)
Но я думаю, что это выходит за рамки этого вопроса. Я могу найти некоторые ссылки, если вы хотите узнать больше.
Классы типов, с другой стороны, являются способом определения полиморфного поведения. Грубо типы классов - это контракты, которые предоставляет определенный тип. Например, вы знаете, что ваше значение x
удовлетворяет контракту, который определяет какое-либо действие. Затем вы можете вызвать этот метод, и фактическая реализация этого контракта затем выбирается автоматически.
классы Обычно набираемые по сравнению с Java-интерфейсов, например:
-- Haskell
class Show a where
show :: a -> String
// Java
public interface Show {
String show();
}
// Scala
trait Show {
def show: String
}
Используя это сравнение, экземпляры классов типа совпадают с реализацией интерфейсов:
-- Haskell
data AB = A | B
instance Show AB where
show A = "A"
show B = "B"
// Scala
sealed trait AB extends Show
case object A extends AB {
val show = "A"
}
case object B extends AB {
val show = "B"
}
Есть очень имп ortant различия между интерфейсами и классами классов.Во-первых, вы можете написать собственный класс типа и сделать любой тип экземпляром этого:
class MyShow a where
myShow :: a -> String
instance MyShow Int where
myShow x = ...
Но вы не можете делать такие вещи с интерфейсами, то есть, вы не можете сделать существующий класс реализовать интерфейс. Эта функция, как вы также заметили, означает, что классы типов: open.
Эта возможность добавления экземпляра класса типа для существующих типов является способом решения expression problem. Язык Java не имеет средств для его решения, но у Haskell, Scala или Clojure есть.
Еще одно отличие классов классов и интерфейсов заключается в том, что интерфейсы являются полиморфными только при первом аргументе, а именно: неявным this
. Типовые классы в этом смысле не ограничены. Вы можете определить типы классов, которые отправляются даже по возвращенному значению:
class Read a where
read :: String -> a
Это невозможно сделать с помощью интерфейсов.
Типы классов могут быть эмулированы в Scala с использованием неявных параметров. Этот шаблон настолько полезен, что в последних версиях Scala существует даже специальный синтаксис, который упрощает его использование. Вот как это делается:
trait Showable[T] {
def show(value: T): String
}
object ImplicitsDecimal {
implicit object IntShowable extends Showable[Int] {
def show(value: Int) = Integer.toString(value)
}
}
object ImplicitsHexadecimal {
implicit object IntShowable extends Showable[Int] {
def show(value: Int) = Integer.toString(value, 16)
}
}
def showValue[T: Showable](value: T) = implicitly[Showable[T]].show(value)
// Or, equivalently:
// def showValue[T](value: T)(implicit showable: Showable[T]) = showable.show(value)
// Usage
{
import ImplicitsDecimal._
println(showValue(10)) // Prints "10"
}
{
import ImplicitsHexadecimal._
println(showValue(10)) // Prints "a"
}
Showable[T]
черты соответствует типу классу, так и неявных объекты определение соответствует его экземплярам.
Как вы можете видеть, типы классов - это своего рода интерфейс, но более мощный. Вы даже можете выбрать различные реализации классов типов, в то время как код, который их использует, остается тем же. Однако эта мощность идет за счет шаблонов и дополнительных объектов.
Обратите внимание, что можно записать эквивалент Haskell выше программы Scala, но для этого потребуется писать несколько модулей или обертки newtype
, поэтому я не представляю их здесь.
BTW, Clojure, диалоги Lisp, работающие на JVM, имеют протоколы , которые объединяют интерфейсы и классы типов. Протоколы отправляются по одному первому аргументу, но вы можете реализовать протокол для любого существующего типа.
Вы имеете в виду * алгебраические * типы данных? – Carl
Извините, я думаю, что в мире ОО ADT имеет тенденцию означать абстрактный тип данных, но в мире FP это означает Алгебраический тип данных, поэтому я немного запутался. Спасибо всем за это. –