Почему плохо?

У Haskell есть магическая функция с именем seq, которая принимает аргумент любого типа и сводит его к нормальной форме слабой головы (WHNF).

Я прочитал несколько источников [не то, что я могу вспомнить, кем они были сейчас...], которые утверждают, что "полиморфный seq плохой". В каком смысле они "плохие"?

Аналогично, существует функция rnf, которая сводит аргумент к нормальной форме (NF). Но это метод класса; он не работает для произвольных типов. Мне кажется "очевидным", что можно изменить спецификацию языка, чтобы обеспечить это как встроенный примитив, похожий на seq. Вероятно, это будет "еще хуже", чем просто seq. Каким образом это так?

Наконец, кто-то предположил, что предоставление seq, rnf, par и подобный же тип, что и функция id, а не функция const, как сейчас, будет улучшением. Как это так?

Ответ 1

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

map g (f xs) = f (map g xs)

выполняется для всех функций g :: tau -> tau', все списки xs :: [tau] и все полиморфные функции f :: [a] -> [a]. В принципе, это равенство означает, что f может только изменять порядок элементов своего списка аргументов или отбрасывать или дублировать элементы, но не может изобретать новые элементы.

Честно говоря, он может изобретать элементы, так как он может "вставлять" в списки недопустимое вычисление/время выполнения, поскольку тип ошибки является полиморфным. То есть, это равенство уже ломается на языке программирования, таком как Haskell без seq. Следующие определения функций предоставляют встречный пример для уравнения. В принципе, с левой стороны g "скрывает" ошибку.

g _ = True
f _ = [undefined]

Чтобы исправить уравнение, g должен быть строгим, т.е. он должен отображать ошибку в ошибку. В этом случае снова выполняется равенство.

Если вы добавите полиморфный оператор seq, уравнение снова разрывается, например, следующее экземплярирование - это примерный пример.

g True = True
f (x:y:_) = [seq x y]

Если мы рассмотрим список xs = [False, True], то

map g (f [False, True]) = map g [True] = [True]

но, с другой стороны,

f (map g [False, True]) = f [undefined, True] = [undefined]

То есть вы можете использовать seq, чтобы элемент определенной позиции списка зависел от определения другого элемента в списке. Равенство выполняется снова, если g является полным. Если вас интересуют бесплатные теоремы, ознакомьтесь с свободным теоретическим генератором, который позволяет указать, рассматриваете ли вы язык с ошибками или даже язык с seq. Хотя это может показаться менее практичным, seq прерывает некоторые преобразования, которые используются для улучшения работы функциональных программ, например foldr/build слияние происходит в присутствии seq. Если вас интересуют более подробно о свободных теоремах в присутствии seq, ознакомьтесь с Свободные теоремы в присутствии seq.

Насколько я знаю, было известно, что полиморфный seq прерывает определенные преобразования, когда он был добавлен к языку. Однако у африкативов есть и недостатки. Если вы добавите класс типа seq, вам может потребоваться добавить в вашу программу множество ограничений типа типа, если вы добавите seq где-нибудь вглубь. Кроме того, не было выбора опустить seq, как уже было известно, что существуют утечки пространства, которые могут быть исправлены с использованием seq.

Наконец, я могу что-то пропустить, но я не вижу, как будет работать оператор seq типа a -> a. Ключ seq заключается в том, что он вычисляет выражение для выражения нормальной формы, если другое выражение оценивается, чтобы возглавить нормальную форму. Если seq имеет тип a -> a, то нет возможности сделать оценку одного выражения зависящей от оценки другого выражения.

Ответ 2

Другой контрпример представлен в этом ответе - монады не могут удовлетворить законы монады с помощью seq и undefined. А так как undefined не может быть исключено на языке Turing-complete, виновным является seq.