Как ленивая оценка заставила Haskell быть чистым

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

Итак, я хотел бы понять, как ленивая стратегия оценки заставила их сохранить Haskell чистым, а не строгой оценкой stragegy?

Ответ 1

Я думаю, что ответ Jubobs уже суммирует его хорошо (с хорошими ссылками). Но, по-моему, я думаю, что SPJ и друзья ссылаются на это:

Необходимость пройти через этот "монад" бизнес может быть действительно неудобным время от времени. Огромный объем вопросов о переполнении стека, спрашивающий "как мне просто удалить эту вещь IO?" свидетельствует о том, что иногда вы действительно хотите просто распечатать это значение прямо здесь — обычно для целей выяснения того, что происходит с настоящим ****!

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

В ленивом языке, таком как Haskell, этот соблазн все еще существует. Существует много раз, когда было бы очень полезно иметь возможность просто быстро прокрасть этот один маленький эффект здесь или там. Кроме того, что из-за лень добавление эффектов оказывается почти бесполезным. Вы не можете контролировать, когда что-нибудь случится. Даже просто Debug.trace имеет тенденцию давать совершенно непонятные результаты.

Короче говоря, если вы разрабатываете ленивый язык, вам действительно нужно придумать последовательную историю того, как вы обрабатываете эффекты. Вы не можете просто пойти "meh, мы будем притворяться, что эта функция просто волшебна"; без возможности более точно управлять эффектами, вы мгновенно окажетесь в ужасном беспорядке!

TL; DR На нетерпеливом языке вы можете уйти от обмана. На ленивом языке вам действительно нужно делать что-то правильно или просто не работает.

И именно поэтому мы наняли Alex — wait, неправильное окно...

Ответ 2

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

Вот соответствующий отрывок из статьи История Хаскелла: быть ленивым с классом:

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

Чистота - большая ставка, с повсеместными последствиями. Неограниченные побочные эффекты, несомненно, очень удобны. Отсутствие побочных эффектов, ввод/вывод Haskells изначально был больно неуклюжим, что было источником значительного смущения. Необходимость, являющаяся матерью изобретения, это смущение в конечном итоге привело к изобретению монадического ввода-вывода, который мы теперь рассматриваем как один из основных вкладов Хаскелла в мир, как мы более подробно обсудим в разделе 7.

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

(мой акцент)

Я также приглашаю вас послушать 18'30 '' Программный радиоканал подкаст # 108 для объяснения самого человека. И вот более длинный, но релевантный отрывок из интервью SPJ в Питер Сейбел Кодерс на работе:

Теперь я думаю, что важная вещь о лени заключается в том, что она держала нас в чистоте. [...]

[...], если у вас ленивый оценщик, его сложнее предсказать, когда будет оцениваться выражение. Таким образом, это означает, что если вы хотите напечатать что-то на экране, каждый язык по каждому значению, где порядок оценки полностью явный, делает это с помощью нечистой "функции" - помещает кавычки вокруг него, потому что теперь он не является функция вообще - с типом типа, похожим на строку на единицу. Вы вызываете эту функцию, и в качестве побочного эффекта она помещает что-то на экран. То, что происходит в Lisp; это также происходит в ML. Это происходит по существу в каждом языке по каждому значению.

Теперь на чистом языке, если у вас есть функция от строки до единицы, вам никогда не нужно будет ее вызывать, потому что вы знаете, что она просто дает блок ответа. То, что может сделать любая функция, дает вам ответ. И вы знаете, что такое ответ. Но, конечно, если у него есть побочные эффекты, очень важно, чтобы вы его назвали. На ленивом языке проблема в том, что вы говорите: "f применяется для печати" привет "," то ли f оценивает свой первый аргумент, не явствует вызывающей функции. Это как-то связано с внутренностями функции. И если вы передадите ему два аргумента, f напечатайте "привет" и напечатайте "до свидания", тогда вы можете напечатать либо или оба в любом порядке, либо ни один. Так что как-то, с ленивой оценкой, делать ввод/вывод побочным эффектом просто невозможно. Таким образом, вы не можете писать разумные, надежные, предсказуемые программы. Таким образом, мы должны были смириться с этим. Это было немного смущающе, потому что вы не могли действительно делать какие-либо ввод/вывод, о которых можно было бы говорить. Поэтому в течение долгого времени у нас по существу были программы, которые могли бы просто взять строку в строку. Это и сделала вся программа. Входной строкой был ввод, а строка результата - вывод, и все, что действительно могла сделать программа.

Вы можете получить немного умный, сделав выводную строку кодировкой некоторых команд вывода, которые интерпретировались каким-то внешним интерпретатором. Таким образом, строка вывода может сказать: "Распечатайте это на экране, поместите это на диск". На самом деле интерпретатор мог это сделать. Таким образом, вы представляете, что функциональная программа - все красиво и чисто, и это своего рода интерпретатор зла, который интерпретирует последовательность команд. Но тогда, конечно, если вы прочтете файл, как вы получите вход обратно в программу? Ну, это не проблема, потому что вы можете выводить строку команд, которые интерпретируются злым интерпретатором и используют ленивую оценку, она может возвращать результаты обратно во вход программы. Таким образом, программа теперь берет поток ответов на поток запросов. Поток запросов идет к злой интерпретатору, который делает вещи миру. Каждый запрос генерирует ответ, который затем возвращается обратно на вход. И поскольку оценка является ленивой, программа выдала ответ как раз вовремя, чтобы он опустился вокруг цикла и потреблялся как вход. Но это было немного хрупким, потому что, если вы слишком долго потребляли свой ответ, вы получаете какой-то тупик. Потому что вы просите ответа на вопрос, который вы еще не выплюнули из своей задней части.

Точка в этом лень побудила нас в угол, в котором нам приходилось думать о путях вокруг этой проблемы ввода-вывода. Я думаю, что это было чрезвычайно важно. Единственное самое важное в лень - это нас отвезло.

(мой акцент)

Ответ 3

Это зависит от того, что вы подразумеваете под "чистым" в этом контексте.

  • Если для чистого вы имеете в виду, как в чисто функциональном, то то, что @MathematicsOrchid верно: с ленивой оценкой вы не знаете, в какой последовательности выполняются нечистые действия и, следовательно, вы не сможете писать значащие программы и вы вынуждены быть более чистыми (используя монаду IO).

    Однако я считаю, что это не очень удовлетворительно в этом случае. Истинный функциональный язык уже отделяет чистый и нечистый код, поэтому даже строгий должен иметь какой-то IO.

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

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

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

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

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


В мышлении функционально с Haskell Ричард Берд делает именно этот момент. Если мы посмотрим на главу 2, выполните D:

Бобр - страстный оценщик, а Сьюзен - ленивый.

[...]

Какую альтернативу Бивер предпочитает head . filter p . map f?

И ответ говорит:

[...] Вместо определения first p f = head . filter p . map f, Бобр может определить

first :: (b -> Bool) -> (a -> b) -> [a] -> b
first p xs | null xs = error "Empty list"
           | p x = x
           | otherwise = first p f (tail xs)
           where x = f (head xs)

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

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

Ответ 4

Строго говоря, это утверждение неверно, потому что Haskell имеет unsafePerformIO, что является большой дырой в функциональной чистоте языка. (Он использует дыру в функциональной чистоте GHC Haskell, которая в конечном итоге восходит к решению реализовать нерассчитанную арифметику, добавив строгий фрагмент к языку). unsafePerformIO существует, потому что соблазн сказать "хорошо, я реализую только эту функцию, используя побочные эффекты внутри", непреодолимо для большинства программистов. Но если вы посмотрите на недостатки unsafePerformIO [1], вы точно увидите, что говорят люди:

  • unsafePerformIO a не гарантируется выполнение a.
  • Не гарантируется выполнение a только один раз, если он выполняется.
  • Также нет никаких гарантий относительно относительного упорядочения ввода-вывода, выполняемого a с другими частями программы.

Эти недостатки сохраняют unsafePerformIO в основном ограниченным самым безопасным и самым тщательным использованием, и поэтому люди используют IO непосредственно, пока это становится слишком неудобным.

[1] Помимо типа-небезопасности; let r :: IORef a; r = unsafePerformIO $ newIORef undefined дает вам полиморфный r :: IORef a, который можно использовать для реализации unsafeCast :: a -> b. ML имеет решение для ссылочного распределения, которое позволяет избежать этого, и Haskell мог бы решить его аналогичным образом, если бы чистота не считалась желательной в любом случае (ограничение мономорфизма - это почти решение в любом случае, вам просто нужно запретить людям работать вокруг него используя сигнатуру типа, как я сделал выше).