Зачем нам нужны "Алгебраические типы данных"?
Я прочитал некоторое объяснение типов алгебраических данных:
В этих статьях приведены подробные описания и примеры кода.
Сначала я думал, что Algebraic Data Type предназначен только для простого определения некоторых типов, и мы можем сопоставить их с сопоставлением с образцом. Но после прочтения этих статей я обнаружил, что "сопоставление с образцом" даже не упоминается там, и контент выглядит интересным, но гораздо более сложным, чем я ожидал.
Итак, у меня есть некоторые вопросы (на которые в этих статьях не отвечают):
- Зачем нам это нужно, скажем, в Haskell или Scala?
- Что мы можем сделать, если у нас есть это, и что мы не сможем сделать, если у нас его нет?
Ответы
Ответ 1
Мы должны начать с статьи wiki Haskell Алгебраические типы данных
И вот, коротко, просто мое видение:
- нам нужно, чтобы они моделировали бизнес-логику старым объектно-ориентированным способом (или фактически старым способом на основе категорий) и делали его более типичным, поскольку компилятор может проверить, что вы соответствовали всем возможным выборам. Или, другими словами, ваша функция total, а не частичная. В конце концов, это дает компилятору возможность подтвердить правильность вашего кода (что рекомендуется использовать запечатанные черты). Таким образом, у более разных типов у вас есть лучшее - btw, общее программирование помогает вам здесь, поскольку оно создает больше типов.
- Стандартные функции: мы можем представлять тип как "набор" объектов, мы можем сопоставлять объект с типом (с использованием сопоставления с образцом) или даже деконструировать (анализировать) его с помощью сокетов. Мы также можем динамически добавлять поведение (во время компиляции) к такому типу с классом classess. Это также возможно для обычного класса, но здесь он дает нам возможность отделять алгебраическую модель (типы) от поведения (функций)
- мы можем строить типы как продукты/coproducts разных объектов/типов. Вы можете думать о системе алгебраического типа как о наборе (или, более общо, как о декартовой замкнутой категории).
type Boolean = True | False
означает, что Boolean является объединением (копроизведением) True
и False
. Cat(height: Height, weight: Weight)
- это "кортеж" (более общий - продукт) Height
и Weight
. Продукт (более-менее) представляет собой "часть" отношений от ООП, копродукт - "есть" (но наоборот).
Он также дает нам способ отправить поведение во время выполнения в многоточечном стиле (например, в CLOS):
sealed trait Animal
case class Cat extends Animal
case class Dog extends Animal
def move(c: Animal) = c match {
case Cat(...) => ...
case Dog(...) =>
case a: Animal => ...//if you need default implementation
}
Haskell, как:
data Animal = Dog | Cat //coproduct
let move Dog d = ...
let move Cat c = ...
Вместо:
trait Animal {
...
def move = ...
}
class Cat(val ...) extends Animal {
override def move = ...
}
class Dog(val ...) extends Animal {
override def move = ...
}
P.S. Теоретически, если вы моделируете мир алгебраическим образом, а ваши функции полны и чисты - вы можете доказать, что ваше приложение правильно. Если он компилируется - он работает:).
P.S.2. Следует также упомянуть, что иерархия типов Scala, которая имеет Any
в ней, не очень хороша для типов безопасности (но хороша для взаимодействия с Java и IO), поскольку она разрушает красивую структуру, определенную GADT. Более того, case class
может быть как GADT (алгебраическим), так и ADT (абстрактным), что также снижает гарантии.
Ответ 2
Сообщение в блоге, о котором вы упоминаете, больше связано с математическими аспектами алгебраических типов данных, которые касаются их практического использования в программировании. Я думаю, что большинство функциональных программистов впервые узнали об алгебраических типах данных, используя их на каком-то языке программирования и только позже изучили их алгебраические свойства.
В самом деле, намерение сообщения в блоге ясно с самого начала:
В этой серии сообщений я объясню, почему типы данных Haskells называемой алгебраической - без упоминания теории категорий или математика.
В любом случае, практическую полезность алгебраических типов лучше всего оценивать, играя с ними.
Предположим, например, что вы хотите написать функцию для пересечения двух сегментов на плоскости.
def intersect(s1: Segment, s2: Segment): ???
Каким должен быть результат? Заманчиво писать
def intersect(s1: Segment, s2: Segment): Point
но что, если нет пересечения? Можно попытаться исправить этот угловой регистр, вернув null
или выбросив исключение NoIntersection
. Однако два сегмента могут также перекрываться более чем на одну точку, когда они лежат на одной прямой. Что мы должны делать в таких случаях? Выбрасывание другого исключения?
Подход к алгебраическим типам заключается в разработке типа, охватывающего все случаи:
sealed trait Intersection
case object NoIntersection extends Intersection
case class SinglePoint(p: Point) extends Intersection
case class SegmentPortion(s: Segment) extends Intersection
def intersect(s1: Segment, s2: Segment): Intersection
Существует много практических случаев, когда такой подход кажется вполне естественным. На некоторых других языках, не имеющих алгебраических типов, приходится прибегать к исключениям, к null
(также см. ошибка в миллиард долларов) (что делает невозможным компилятор проверить исчерпываемость соответствия шаблонов) или другим функциям, предоставляемым языком. Возможно, "лучший" вариант в ООП заключается в использовании шаблона посетителя для кодирования алгебраических типов и сопоставления образцов на языках, которые не имеют таких функций. Тем не менее, наличие того, что непосредственно поддерживается на языке, как и в scala, гораздо удобнее.