Какую проблему решают алгебраические типы данных?

18

Честное предупреждение, я новичок в функциональном программировании, поэтому я могу придерживаться многих неверных предположений.

Я изучал алгебраические типы. Кажется, что многие функциональные языки имеют их, и они довольно полезны в сочетании с сопоставлением с образцом. Однако какую проблему они на самом деле решают? Я могу реализовать на первый взгляд (вроде) алгебраический тип в C # следующим образом:

public abstract class Option { }
public class None : Option { }
public class Some<T> : Option
{
    public T Value { get; set; }
}

var result = GetSomeValue();
if(result is None)
{
}
else
{
}

Но я думаю, что большинство согласится с тем, что это ублюдение объектно-ориентированного программирования, и вы никогда не должны этого делать. Так функциональное программирование просто добавляет более чистый синтаксис, который делает этот стиль программирования менее грубым? Что еще мне не хватает?

ConditionRacer
источник
6
Функциональное программирование - это другая парадигма, чем объектно-ориентированное программирование.
Василий Старынкевич
@BasileStarynkevitch Я понимаю, но есть такие языки, как F #, которые являются и тем, и другим. Вопрос не столько в функциональных языках, сколько в том, какую проблему решают алгебраические типы данных.
ConditionRacer
7
Что произойдет, когда я определю class ThirdOption : Option{}и укажу, new ThirdOption()где вы ожидали Someили None?
amon
1
У @amon языков, которые имеют типы суммы, обычно есть способы запретить это. Например, Haskell определяет data Maybe a = Just a | Nothing(эквивалентно data Option a = Some a | Noneв вашем примере): вы не можете добавить третий случай post-hoc. В то время как вы можете эмулировать типы сумм в C #, как вы показали, это не самая красивая.
Мартейн
1
Я думаю, что это не «какую проблему решают ADT», а скорее «ADT - это другой подход к вещам».
Математическая

Ответы:

44

Классы с интерфейсами и наследованием представляют открытый мир: любой может добавить новый тип данных. Для данного интерфейса могут существовать классы, реализующие его по всему миру, в разных файлах, в разных проектах, в разных компаниях. Они упрощают добавление наблюдений в структуры данных, но поскольку реализации интерфейса децентрализованы, трудно добавить новый метод в интерфейс. Когда интерфейс общедоступен, он в основном заморожен. Никто не знает всех возможных реализаций.

Алгебраические типы данных двойственны, они замкнуты . Все случаи данных перечислены в одном месте , и операции , не только могут перечислить варианты исчерпывающе, они поощряются сделать это. Следовательно, написание новой функции, работающей с алгебраическим типом данных, тривиально: просто напишите чертову функцию. В свою очередь, добавление новых случаев является сложным, потому что вам нужно пересмотреть в основном всю кодовую базу и расширить каждый match. Как и в случае с интерфейсами, в стандартной библиотеке Rust добавление нового варианта является серьезным изменением (для открытых типов).

Это две стороны проблемы выражения . Алгебраические типы данных являются их неполным решением, как и ООП. Оба имеют свои преимущества в зависимости от того, сколько случаев данных существует, как часто эти случаи меняются, и как часто операции расширяются или изменяются. (Именно поэтому многие современные языки предоставляют оба или что-то подобное, или идут прямо к более мощным и более сложным механизмам, которые пытаются объединить оба подхода.)


источник
Получаете ли вы ошибку компилятора, если не удается обновить совпадение, или это происходит только во время выполнения?
Ян
6
@Ian Большинство функциональных языков имеют статическую типизацию и проверяют исчерпывающее соответствие шаблонов. Однако, если существует шаблон «поймать все», компилятор будет счастлив, даже если функции придется работать с новым случаем, чтобы выполнить свою работу. Кроме того, вам нужно перекомпилировать весь зависимый код, вы не можете скомпилировать только одну библиотеку и повторно связать ее с уже созданным приложением.
Кроме того, статическая проверка того, что шаблон является исчерпывающим, в целом является дорогостоящей. Он в лучшем случае решает проблему SAT, а в худшем - язык допускает произвольные предикаты, что делает это неразрешимым.
USR
@usr Нет языка, о котором я знаю, я пытаюсь найти идеальное решение, либо компилятор понимает, что оно исчерпывающее, либо вы вынуждены добавить универсальный случай, когда вы терпите крах и сгораете. Я не знаю отношения к SAT, есть ли у вас связь с сокращением? Независимо от того, для реального кода, написанного в реальных программах, проверка исчерпываемости - это капля в море.
Представьте, что вы совпадаете с N булевыми значениями. Затем вы добавляете предложения соответствия, такие как (a, b, _, d, ...). Тогда всеобщее дело - это! Clause1 &&! Clause2 && .... Для меня это похоже на SAT.
USR
12

Так функциональное программирование просто добавляет более чистый синтаксис, который делает этот стиль программирования менее грубым?

Возможно, это упрощение, но да.

Что еще мне не хватает?

Давайте разберемся, что такое алгебраические типы данных (суммируя эту прекрасную ссылку из «Learn you as Haskell»):

  • Тип суммы, который говорит, что «это значение может быть A или B».
  • Тип продукта, который говорит, что «это значение - и A и B».

Ваш пример действительно работает только с первым.

Возможно, вам не хватает того, что, предоставляя эти две основные операции, функциональные языки позволяют создавать все остальное. C # имеет структуры, классы, перечисления, обобщения, а также кучу правил, управляющих поведением этих вещей.

В сочетании с некоторым синтаксисом, чтобы помочь, функциональные языки могут разложить операции по этим двум путям, обеспечивая чистый, простой и элегантный подход к типам.

Какую проблему решают алгебраические типы данных?

Они решают ту же проблему, что и любая другая система типов: «какие значения здесь разрешено использовать?» - они просто придерживаются другого подхода.

Telastyn
источник
4
Ваше последнее предложение вроде как сказать кому-то, что «корабли решают ту же проблему, что и самолеты: транспорт - они просто используют другой подход». Это совершенно правильное утверждение, а также довольно бесполезное.
Мердад
@ mehrdad - Я думаю, это немного преувеличивает.
Теластин
1
Без сомнения, вы прекрасно понимаете алгебраические типы данных, но ваш маркированный список («Тип суммы ...» и «Тип продукта ...») больше похож на описание типов объединения и пересечения, которые не являются Совершенно то же самое, что сумма и тип продукта.
пион
6

Вас может удивить, что сопоставление с образцом не считается самым идиоматичным способом работы с опциями. Смотрите документацию по настройкам Scala для более подробной информации. Я не уверен, почему так много учебных пособий по FP поощряют такое использование.

В основном вам не хватает того, что существует целый набор функций, созданных для облегчения работы с опциями. Рассмотрим основной пример из документов Scala:

val name: Option[String] = request getParameter "name"
val upper = name map { _.trim } filter { _.length != 0 } map { _.toUpperCase }
println(upper getOrElse "")

Обратите внимание на то, как mapи filterразрешить вам цепочку операций с опциями, без необходимости проверять в каждой точке, есть ли у вас Noneили нет. Затем в конце вы используете, getOrElseчтобы указать значение по умолчанию. Ни в коем случае вы не делаете что-то «грубое», например, проверку типов. Любая неизбежная проверка типов выполняется внутри библиотеки. У Haskell есть свой набор аналогичных функций, не говоря уже о большом наборе функций, которые будут работать с любой монадой или функтором.

Другие алгебраические типы данных имеют свои собственные идиоматические способы работы с ними, и сопоставление с образцом в большинстве случаев является низким на полюсе тотема. Аналогично, когда вы создаете свои собственные типы, вы должны предоставлять аналогичные функции для работы с ними.

Карл Билефельдт
источник