Почему "голова" Хаскелла падает на пустой список (или почему *не* он возвращает пустой список)? (Философия языка )
Примечание для других потенциальных участников: пожалуйста, не стесняйтесь использовать абстрактные или математические обозначения, чтобы сделать вашу точку зрения. Если я найду ваш ответ неясным, я попрошу разъяснений, но в остальном не стесняйтесь выражать себя в удобной форме.
чтобы быть ясным: я не ищу "безопасный" head, и это не выбор head в частности, исключительно значимой. Мясо вопроса следует за обсуждением head и head', которые служат для обеспечения контекста.
Я уже несколько месяцев взламываю Haskell (до такой степени, что он стал моим основным языком), но я, по общему признанию, не очень хорошо информирован о некоторых более продвинутых концепциях и деталях философии языка (хотя я более чем готов учиться). Мой вопрос тогда не столько технический (если это не так, и я просто не понимаю этого), сколько философский.
для этого примера, я говорю о head.
как я полагаю, вы будете знать,
Prelude> head []
*** Exception: Prelude.head: empty list
это следует из head :: [a] -> a. Справедливо. Очевидно, что нельзя вернуть элемент (махнув рукой) без типа. Но в то же время, это просто (если не тривиально) определить
head' :: [a] -> Maybe a
head' [] = Nothing
head' (x:xs) = Just x
Я видел небольшое обсуждение этого здесь в разделе комментариев некоторых утверждений. Примечательно, что один Алекс Штангл говорит
'есть веские причины не делать все "безопасный" и бросать исключения, когда нарушаются условия.-
Я не обязательно подвергаю сомнению это утверждение, но мне любопытно, каковы эти "веские причины".
кроме того, Пол Джонсон говорит:
'например, вы можете определить" safeHead:: [a] -> Maybe a", но теперь вместо того, чтобы обрабатывать пустой список или доказывать, что это не может произойти, вам нужно обработать "ничего" или доказать, что это не может произойти.-
в тон, который я прочитал из этого комментария, предполагает, что это заметное увеличение сложности/сложности/чего-то, но я не уверен, что понимаю, что он там ставит.
один Стивен пружина говорит (в 2011 году, не меньше),
есть более глубокая причина, по которой, например, "голова" не может быть устойчива к краху. Чтобы быть полиморфным, но обрабатывать пустой список, 'head' всегда должен возвращать переменную типа, который отсутствует в любом конкретном пустом списке. Это было бы Дельфийским, если бы Хаскелл мог сделать это...".
полиморфизм теряется, позволяя обрабатывать пустой список? Если так, как же так, и почему? Есть ли конкретные случаи, которые сделали бы это очевидным? на этот раздел подробно ответил @Russell O'Connor. любые дальнейшие мысли, конечно, оценили.
Я буду редактировать это как ясность и диктует предложение. Любые мысли, документы и т. д., вы можете предоставить будет наиболее ценится.
6 ответов:
полиморфизм теряется, позволяя пустой обработка списка? Если так, как же так, и почему? Есть ли конкретные случаи, которые бы сделать это очевидным?
свободная теорема для
headутверждает, чтоf . head = head . $map fприменение этой теоремы к
[]означает, чтоf (head []) = head (map f []) = head []эта теорема должна выполняться для каждого
f, так что в частности он должен держать дляconst Trueиconst False. Это подразумеваетTrue = const True (head []) = head [] = const False (head []) = Falseтаким образом, если
headправильно полиморфный иhead []общее значение, тоTrueбудет равнаFalse.PS. У меня есть некоторые другие комментарии о предыстории вашего вопроса о том, что если у вас есть предварительное условие, что ваш список непустой, то вы должны применить его, используя непустой тип списка в вашей сигнатуре функции вместо использования списка.
почему кто-то использовать
head :: [a] -> aвместо сопоставления с образцом? Одна из причин заключается в том, что вы знаете, что аргумент не может быть пустым и не хотите писать код для обработки случая, когда аргумент пуст.конечно, ваш
head'типа[a] -> Maybe aопределен в стандартной библиотеке какData.Maybe.listToMaybe. Но если вы замените использованиеheadСlistToMaybe, вы должны написать код для обработки пустого случая, который побеждает эту цель использованияhead.Я не говорю, что с помощью
headхороший стиль. Он скрывает тот факт, что это может привести к исключению, и в этом смысле это не хорошо. Но иногда это удобно. Дело в том, чтоheadслужит некоторым целям, которым не может служитьlistToMaybe.последняя цитата в вопросе (о полиморфизме) просто означает, что невозможно определить функцию типа
[a] -> aкоторый возвращает значение в пустом списке (как Рассел О'Коннор пояснил, что в ответ).
вполне естественно ожидать, что следующее удержится:
xs === head xs : tail xs- список идентичен своему первому элементу, за которым следуют остальные. Кажется логичным, не так ли?теперь давайте подсчитаем количество минусов (приложений
:), игнорируя фактические элементы, применяя предполагаемый "закон" к[]:[]должно быть идентичноfoo : bar, но первый имеет 0 минусов, в то время как последний имеет (по крайней мере) один. Ой, что-то здесь не так!Хаскелл тип системы, при всех ее сильных сторонах, не дотягивает до выражения того, что нужно только вызвать
headв непустом списке (и что "закон" действителен только для непустых списков). Используяheadпереносит бремя доказательства на программиста, который должен убедиться, что он не используется в пустых списках. Я считаю, что зависимо типизированные языки, такие как Agda, могут помочь здесь.наконец, чуть более операционально-философское описание: как должно
head ([] :: [a]) :: aбудет осуществляться? Колдовать значение типаaиз воздуха невозможно (подумайте о необитаемых типах, таких какdata Falsum), и было бы равносильно доказательству чего-либо (через изоморфизм Карри-Говарда).
есть несколько различных способов, чтобы думать об этом. Так что я буду спорить и за, и против
head':против
head':нет необходимости иметь
head': поскольку списки являются конкретным типом данных, все, что вы можете сделать сhead'вы можете сделать по шаблону.кроме того, с
head'вы просто меняете один функтор на другой. В какой-то момент Вы хотите ближе к делу и поработать на Базовый элемент списка.в защиту
head':но сопоставление с образцом скрывает то, что происходит. В Haskell мы заинтересованы в вычислении функций, что лучше всего достигается путем написания их в стиле без точек с использованием композиций и комбинаторов.
кроме того, думая о
[]иMaybeфункторы,head'позволяет перемещаться вперед и назад между ними (в частности,Applicativeэкземпляр[]Сpure = replicate.)
Если в вашем случае использования пустой список не имеет никакого смысла, вы всегда можете использовать
NonEmpty, гдеneHeadбезопасен в использовании. Если вы видите его под этим углом, это неheadфункция, которая небезопасна, это вся структура данных списка (опять же, для этого случая использования).
Я думаю, это вопрос простоты и красоты. Что, конечно, в глазах смотрящего.
если исходить из фона Lisp, вы можете знать, что списки построены из ячеек cons, каждая ячейка имеет элемент данных и указатель на следующую ячейку. Пустой список-это не список как таковой, а особым символом. И Хаскелл идет с этим рассуждением.
на мой взгляд, это и чище, проще рассуждать о, и более традиционный, если пустой список и список два разные вещи.
...Я могу добавить - если вы беспокоитесь о том, что голова небезопасна - не используйте ее, вместо этого используйте сопоставление шаблонов:
sum [] = 0 sum (x:xs) = x + sum xs