Зачем нам нужны "Алгебраические типы данных"?

Я прочитал некоторое объяснение типов алгебраических данных:

В этих статьях приведены подробные описания и примеры кода.

Сначала я думал, что 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, гораздо удобнее.