Подтвердить что ты не робот

Почему в Haskell выражаются недовольства?

Это был вопрос, который я задавал некоторое время. если утверждения являются главными в большинстве языков программирования (по крайней мере, с теми, с которыми я работал), но в Haskell кажется, что это довольно неодобрительно. Я понимаю, что для сложных ситуаций совпадение шаблонов Haskell намного чище, чем куча ifs, но есть ли какая-то реальная разница?

Для простого примера возьмите самодельную версию суммы (да, я знаю, что это может быть только foldr (+) 0):

sum :: [Int] -> Int
-- separate all the cases out
sum [] = 0
sum (x:xs) = x + sum xs

-- guards
sum xs
    | null xs = 0
    | otherwise = (head xs) + sum (tail xs)

-- case
sum xs = case xs of
    [] -> 0
    _ -> (head xs) + sum (tail xs)

-- if statement
sum xs = if null xs then 0 else (head xs) + sum (tail xs)

Как второй вопрос, какой из этих вариантов считается "лучшей практикой" и почему? Мой профессор вернулся, когда всегда использовал первый метод, когда это было возможно, и мне интересно, только ли это его личное предпочтение или что-то позади.

4b9b3361

Ответ 1

Проблема с вашими примерами - это не выражения if, это использование частичных функций, таких как head и tail. Если вы попытаетесь вызвать любой из них с пустым списком, он выдает исключение.

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

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

Например, скажем, вы случайно переключили части вашей функции then и else.

-- Compiles, throws error at run time.
sum xs = if null xs then (head xs) + sum (tail xs) else 0

-- Doesn't compile. Also stands out more visually.
sum [] = x + sum xs
sum (x:xs) = 0

Обратите внимание, что ваш пример с защитой имеет ту же проблему.

Ответ 2

Я думаю, что статья Boolean Blindness очень хорошо отвечает на этот вопрос. Проблема в том, что логические значения потеряли все семантическое значение, как только вы их построите. Это делает их отличным источником ошибок, а также затрудняет понимание кода.

Ответ 3

Ваша первая версия, которую предпочитает ваш профессор, имеет следующие преимущества по сравнению с другими:

  • не упоминается null Компоненты списка
  • называются в шаблоне, поэтому не упоминается head и tail.

Я думаю, что это считается "лучшей практикой".

Какая большая сделка? Почему мы хотим избежать особенно head и tail? Ну, все знают, что эти функции не являются суммарными, поэтому каждый пытается убедиться, что все случаи охвачены. Совпадение шаблона на [] не только выделяется больше, чем null xs, серия совпадений шаблонов может быть проверена компилятором для полноты. Следовательно, идиоматическая версия с полным совпадением шаблонов легче понять (для обученного читателя Haskell) и доказательством, полным компилятором.

Вторая версия немного лучше последней, потому что сразу видно, что все случаи обрабатываются. Тем не менее, в общем случае RHS второго уравнения может быть длиннее и могут быть предложения where с несколькими определениями, последнее из них может быть примерно таким:

where
   ... many definitions here ...
   head xs = ... alternative redefnition of head ...

Чтобы быть абсолютно уверенным в понимании того, что делает RHS, нужно убедиться, что общие имена не были переопределены.

3-я версия - худшая ИМХО: а) Второй матч не может деконструировать список и по-прежнему использует голову и хвост. б) Случай несколько более подробный, чем эквивалентная запись с двумя уравнениями.

Ответ 4

Во многих языках программирования if-statements являются фундаментальными примитивами, и такие вещи, как блоки-переключатели, являются просто синтаксическим сахаром, чтобы облегчить запись глубоко-вложенных операторов if.

Haskell делает это наоборот. Согласование шаблонов является фундаментальным примитивным, а if-expression - буквально просто синтаксическим сахаром для сопоставления шаблонов. Аналогично, конструкции типа null и head являются просто определяемыми пользователем функциями, которые в конечном итоге реализованы с использованием сопоставления с образцом. Таким образом, соответствие шаблонов - это все, что находится внизу. (И, следовательно, потенциально более эффективно, чем вызов пользовательских функций.)

Во многих случаях, таких как те, которые вы перечисляете выше, это просто вопрос стиля. Компилятор почти наверняка оптимизирует ситуацию до такой степени, что все версии примерно равны по производительности. Но в целом [не всегда!] Сопоставление шаблонов делает более четким то, что вы пытаетесь достичь.

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

Ответ 5

Каждый вызов null, head и tail влечет за собой соответствие шаблона. Но 1-я версия в вашем ответе выполняет только одно совпадение с шаблоном и повторяет его результаты через именованные компоненты шаблона.

Просто для этого лучше. Но это также более визуально, более читаемо.

Ответ 6

Сравнение шаблонов лучше, чем строка инструкций if-then-else для (по крайней мере) следующих причин:

  • это более декларативный
  • он хорошо взаимодействует с типами сумм

Согласование шаблонов помогает уменьшить количество "случайной сложности" в вашем коде, то есть код, который действительно больше связан с деталями реализации, а не с основной логикой вашей программы.

В большинстве других языков, когда компилятор/время выполнения видит строку операторов if-then-else, у нее нет выбора, кроме как проверить условия точно в том порядке, в котором программист указал их. Но сопоставление шаблонов побуждает программиста больше сосредоточиться на описании того, что должно произойти в сравнении с тем, как должны выполняться вещи. Из-за чистоты и неизменности значений в Haskell компилятор может рассмотреть коллекцию шаблонов в целом и решить, как наилучшим образом их реализовать.

Аналогом будет оператор C switch. Если вы сбросите код сборки для различных операторов switch, вы увидите, что иногда компилятор будет генерировать цепочку/дерево сравнений, а в других случаях он будет генерировать таблицу перехода. В обоих случаях программист использует один и тот же синтаксис - компилятор выбирает реализацию на основе значений сравнения. Если они образуют смежный блок значений, используется метод таблицы перехода, в противном случае используется дерево сравнения. И это разделение проблем позволяет компилятору реализовать еще больше стратегий в будущем, если обнаружены другие шаблоны среди значений сравнения.