Чому `голова` Хаскелла падає на порожній список (або чому * не * повертає порожній список)? (Мовна філософія)


76

Примітка іншим потенційним співавторам: Будь ласка, не соромтеся використовувати абстрактні чи математичні позначення, щоб висловити свою думку. Якщо я знайду вашу відповідь незрозумілою, я попрошу її пояснити, але в іншому випадку сміливо висловлюйтеся з комфортом.

Щоб бути зрозумілим: я не шукаю "сейф" head, а також вибір headособливо винятково значущого. Основним питанням є обговорення headі head', які служать для забезпечення контексту.

Вже кілька місяців я хакую Хаскела (до того, що це стало моєю основною мовою), але, правда, я недостатньо обізнаний про деякі більш просунуті концепції та деталі філософії мови (хоча Я більш ніж готовий вчитися). Тоді моє питання не стільки технічне (якщо це не так, і я просто цього не усвідомлюю), скільки питання філософії.

На цьому прикладі я кажу head.

Як я гадаю, ти знатимеш,

Prelude> head []    
*** Exception: Prelude.head: empty list

Це випливає з head :: [a] -> a. Досить справедливо. Очевидно, що не можна повернути елемент (махаючи рукою) жодного типу. Але в той же час визначити це просто (якщо не тривіально)

head' :: [a] -> Maybe a
head' []     = Nothing
head' (x:xs) = Just x

Я бачив невелике обговорення цього питання тут, у розділі коментарів деяких тверджень. Примітно, говорить один Алекс Штангле

"Є вагомі причини не робити все" безпечним "і застосовувати винятки, коли передумови порушуються".

Я не обов'язково ставлю під сумнів це твердження, але мені цікаво, якими є ці "вагомі причини".

Крім того, Пол Джонсон каже:

'Наприклад, ви можете визначити "safeHead :: [a] -> Можливо, a", але тепер замість того, щоб обробляти порожній список або доводити, що цього не може статися, ви повинні обробити "Нічого" або довести, що це не може статися . '

Тон, який я прочитав із цього коментаря, свідчить про те, що це помітне збільшення складності / складності / чогось, але я не впевнений, що розумію, що він там викладає.

Один Стівен Прузіна каже (у 2011 році, не менше),

"Існує глибша причина, чому, наприклад," head "не може бути захищеною від збоїв. Щоб бути поліморфною, але обробляти порожній список," head "завжди повинна повертати змінну типу, яка відсутня в якомусь конкретному порожньому списку. Це було б Дельфічний, якби Хаскелл міг це зробити ... ".

Чи втрачається поліморфізм, дозволяючи обробляти порожній список? Якщо так, то як і чому? Чи існують окремі випадки, які могли б зробити це очевидним? На цей розділ чудово відповів @Russell O'Connor. Будь-які подальші думки, звичайно, вдячні.

Я відредагую це, як це вимагає чіткість та пропозиція. Будь-які думки, статті тощо, які ви можете надати, будуть дуже вдячні.


Також „head“ небезпечний (він частковий, не є загальним), а також не створює виняток (для порожнього списку він не визначений).
Леммінг

Відповіді:


103

Чи втрачається поліморфізм, дозволяючи обробляти порожній список? Якщо так, то як і чому? Чи існують окремі випадки, які могли б зробити це очевидним?

Вільна теорема для 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. У мене є інші зауваження щодо передумови вашого запитання, якщо у вас є передумова, що ваш список не є порожнім, тоді вам слід його застосувати, використовуючи непорожній тип списку у підписі функції, а не використовуючи список.


1
Це саме така відповідь, яку я шукав. Я з нетерпінням чекаю Ваших інших коментарів, якщо Ви хочете їх зробити. :)
Джек Хенахан,

2
І я щойно подивився теореми Уодлера безкоштовно! ще трохи почитати на тему вільних теорем.
Джек Хенахан,

21
Я не розумію, чому це спричиняє проблеми з head :: [a] -> Maybe a, який має вільну теорему, fmap f . head = head . map fі таким чином ви просто отримуєте Nothing == Nothing. Хіба що ви просто намагаєтеся показати, чому неможливо мати default :: forall a . aзначення, яке можна повернути, head []і для якого ми можемо збігатися, але є простіші способи говорити про це.
J. Abrahamson

@ J.Abrahamson cf Data.List.Safe
bjd2385

25

Чому хтось використовує head :: [a] -> aзамість зіставлення зразків? Однією з причин є те, що ви знаєте, що аргумент не може бути порожнім, і ви не хочете писати код для обробки випадку, коли аргумент порожній.

Звичайно, ваш head'тип [a] -> Maybe aвизначається у стандартній бібліотеці як Data.Maybe.listToMaybe. Але якщо замінити використання headз listToMaybe, ви повинні написати код для обробки порожнього корпусу, що суперечить цієї мети використання head.

Я не кажу, що використання head- це хороший стиль. Він приховує той факт, що це може призвести до винятку, і в цьому сенсі це не є добре. Але це буває зручно. Справа в тому, що вона headслужить певним цілям, яким не можуть бути задоволені listToMaybe.

Остання цитата у питанні (про поліморфізм) просто означає, що неможливо визначити функцію типу, [a] -> aяка повертає значення в порожній список (як пояснив Рассел О'Коннор у своїй відповіді ).


8

Цілком природно очікувати, що буде виконуватися наступне: xs === head xs : tail xs- список ідентичний першому елементу, за яким слідує решта. Здається логічним, так?

Тепер давайте підрахуємо кількість похибок (додатків :), не враховуючи фактичних елементів, при застосуванні передбачуваного «закону» до []: []має бути ідентичним foo : bar, але перший має 0 похибок, тоді як другий має (принаймні) одне. Ой, щось тут не так!

Система типів Хаскелла, незважаючи на всі її сильні сторони, не може виражати той факт, що вам слід вказувати лише headнепустий список (і що "закон" діє лише для непустих списків). Використання перекладає headтягар доказування на програміста, який повинен переконатися, що він не використовується у порожніх списках. Я вважаю, що тут можуть допомогти такі набрані мови, як Agda.

Нарешті, трохи більш оперативно-філософський опис: як слід head ([] :: [a]) :: aреалізувати? Викликати значення типу aз повітря неможливо (згадайте про незаселені типи, наприклад data Falsum), і це могло б довести що-небудь (за допомогою ізоморфізму Каррі-Говарда).


4
"означало б доведення чого-небудь (за допомогою ізоморфізму Каррі-Говарда)" Це дуже цікавий момент. Я не думав про такий зв’язок. Ну добре.
Джек Хенахан,

4

Існує ряд різних способів думати про це. Тож я збираюся сперечатися як за, так і проти head':

Проти head':

Немає необхідності мати head': Оскільки списки є конкретним типом даних, все, що ви можете зробити з head'вами, можна зробити шляхом зіставлення шаблонів.

Крім того, head'ви просто обмінюєтеся одним функтором на інший. У якийсь момент ви хочете перейти до латунних прихватів і виконати певну роботу над основним елементом списку.

На захист head':

Але узгодження шаблонів затуляє те, що відбувається. У Haskell ми зацікавлені в обчисленні функцій, що краще досягається шляхом написання їх безточковим стилем за допомогою композицій та комбінаторів.

Крім того, розмірковуючи про функтори []та Maybe, head'дозволяє переміщатися між ними (зокрема, Applicativeприклад []з pure = replicate.)


Хоча, за певною логікою, яка говорить про те, що в цьому взагалі немає потреби, headвсе ж вона залишається. Загалом, мені швидше подобається ваша відповідь, і я був би зацікавлений у будь-яких інших Ваших думках щодо Ваших аргументів.
Джек Хенахан,

Практично кажучи, бувають випадки, коли я хочу, headі інші, коли я хотів би, щоб мав (або я просто визначаю) head'.
MtnViewMark,

3
@MtnViewMark Якщо ви бажаєте head', вам потрібно шукати не далі Data.Maybe.listToMaybe.
Рассел О'Коннор,

Можливо, head'' :: MonadPlus m => [a] -> m aбуло б ще корисніше.
chaosmasttter

3

Якщо у вашому випадку використання порожній список взагалі не має сенсу, ви завжди можете вибрати NonEmptyзамість нього використання , де neHeadбезпечно використовувати. Якщо ви бачите це з цього боку, headнебезпечною є не функція, а ціла структура даних списку (знову ж таки, для цього випадку використання).


1

Я думаю, що це питання простоти та краси. Що, звичайно, на око спостерігача.

Якщо ви надходите з фону Lisp, ви можете знати, що списки будуються з комірок мінусів, кожна комірка має елемент даних та вказівник на наступну комірку. Порожній список - це не список як такий, а спеціальний символ. І Хаскелл йде з цими міркуваннями.

На мій погляд, це і чистіше, і простіше міркувати, і більш традиційне, якщо порожній список і список - це дві різні речі.

... Я можу додати - якщо ви стурбовані тим, що голова небезпечна - не використовуйте її, скоріше використовуйте відповідність шаблону:

sum     [] = 0
sum (x:xs) = x + sum xs

2
О, звичайно, я завжди віддаю перевагу відповідності зразків head. Це швидше цікавість, ніж занепокоєння. До решти Вашого коментаря: Традиційне в якому сенсі? Чия традиція? Чому це традиція?
Джек Хенахан,

Ну, традиція функціонального програмування. Можливо, мені доведеться негайно з'їсти власні слова - у Ліспе (car '()) повертає NIL. Але Haskell був більше спробою об'єднати сучасну натовп функціонального програмування. І як OCaml, так і Міранда (я вважаю) дають помилку, роблячи голову в порожньому списку.
Йохан Котлінський

І саме цю історичну цікавість я намагаюся обґрунтувати. Приблизно ( надзвичайно грубо), []еквівалентно порожньому набору. В математичному контексті, порожня множина є просто набором нічого в ньому. Аналогічним чином , по - , як і раніше є список, і повинна фактично бути список без будь - яких елементів (і , можливо , немає типу). Якщо це (наприклад) обмеження набраного лямбда-числення, це може допомогти пояснити лікування , але я не впевнений, що це так. [][]
Джек Хенахан,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.