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

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

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


person Sibi    schedule 17.07.2015    source источник
comment
Если у вас ленивая оценка, это означает, что любые нечистые действия происходят в случайном порядке. Это своего рода останавливает людей, пытающихся скрыть нечистые действия; действия, которые происходят в случайном порядке, не очень полезны.   -  person MathematicalOrchid    schedule 17.07.2015
comment
@Mat MathematicalOrchid Я этого не понимаю ... Haskell уже разделяет нечистые действия в монаде ввода-вывода, поэтому они упорядочиваются независимо от стратегии оценки ...   -  person Bakuriu    schedule 17.07.2015
comment
@Bakuriu Haskell разделяет нечистые действия на монаду ввода-вывода , потому что в противном случае их порядок был бы полностью произвольным, что является из-за ленивого вычисления. Если бы не ленивая оценка, вы могли бы просто обмануть и написать имитационные функции, которые на самом деле нечисты. Но из-за лени пробовать безнадежно, как бы ни соблазнялись.   -  person MathematicalOrchid    schedule 17.07.2015
comment
В любом случае одна особенность ленивого и строгого вычисления заключается в том, что при ленивом вычислении вы можете мыслить более композиционно (то есть более декларативно / функционально) и получать эффективное решение. Это может оказаться невозможным при использовании строгой стратегии оценки. Например, с ленивым вычислением вы можете делать такие вещи, как: getFirstTrue p = head . filter p, не беспокоясь о том, что он пройдет по всему списку перед возвратом первого элемента. Если вы используете строгую оценку, вы хотите избежать этого. Это становится более важным при смешивании таких вещей, как складки / карты и т. Д.   -  person Bakuriu    schedule 17.07.2015
comment
@Mat MathematicalOrchid С моей точки зрения, Ио имеет смысл разделять чистое и нечистое независимо от стратегии оценки. Это разумно сделать даже строгим языком. То, что он решает даже эту проблему, - это просто плюс.   -  person Bakuriu    schedule 17.07.2015
comment
@Bakuriu Разделить их - очень умный ход. Я хочу сказать, что ленивое оценивание как бы вынуждает вас делать это, в то время как, говоря строгим языком, у вас может возникнуть соблазн обойти разделение только на этот раз, что постепенно приводит к тому, что все время остается. И я думаю, что именно это относится к настроениям, о которых спрашивает ОП.   -  person MathematicalOrchid    schedule 17.07.2015
comment
@Sibi Дело не в том, что ленивое вычисление привело к чистоте; скорее, ленивое вычисление вынудило их сохранить чистоту Haskell. Для информации на каком-то этапе Подкаст Software Engineering Radio № 108 SPJ объясняет, почему чистота сохраняет нас чистыми.   -  person jub0bs    schedule 17.07.2015
comment
@Jubobs Извините, я должен был это правильно сформулировать. Я хотел бы понять, как ленивое вычисление заставило их сохранить чистоту Haskell. Отредактирую вопрос.   -  person Sibi    schedule 17.07.2015


Ответы (4)


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

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

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

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

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

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

И именно поэтому мы наняли Алекса, подождите, не то окно ...

person MathematicalOrchid    schedule 17.07.2015
comment
С другой стороны, обман гораздо более ограничен и, следовательно, гораздо более интересен. Часто проходят годы, прежде чем программист на Haskell разовьет достойное представление о том, когда и как жульничать! - person dfeuer; 23.09.2020

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

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

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

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

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

(мой акцент)

Я также приглашаю вас послушать 18'30 '' из подкаст Software Engineering Radio № 108 с объяснениями, сделанными самим человеком. А вот более длинный, но уместный отрывок из интервью SPJ в Coders at Work Питера Зайбеля. :

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

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

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

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

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

(мой акцент)

person jub0bs    schedule 17.07.2015
comment
Первая ссылка теперь не работает. - person Max Barraclough; 22.09.2020
comment
@MaxBarraclough Спасибо. Я нашел еще одну копию на серверах Microsoft и исправил неработающую ссылку. - person jub0bs; 23.09.2020

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

  • Если для чистого вы имеете в виду, как в чисто функциональном, тогда то, что @Mat MathematicalOrchid истинно: при ленивом вычислении вы не будете знать, в какой последовательности выполняются нечистые действия, и, следовательно, вы бы не вы вообще не можете писать значимые программы, и вы вынуждены быть более чистыми (используя монаду 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.

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

person Bakuriu    schedule 17.07.2015
comment
Я проинформирую Lispers, MLers и Discipline Disciplers, что их предпочтительные языки не являются настоящими функциональными языками. Я думаю, вы просто неправильно понимаете историческую полезность и роль функциональной чистоты. - person Jonathan Cast; 17.07.2015
comment
@jcast Ну, это зависит от того, что вы подразумеваете под функциональным языком. Я хочу сказать, что Haskell был разработан так, чтобы быть очень функциональным, и самый большой шаг в этом направлении - полное разделение между побочными эффектами и оценкой выражений. Итак, да, я определенно сказал бы, что эти языки менее функциональны, чем Haskell, но я не думаю, что Haskell более чистый только потому, что его ленивое вычисление. - person Bakuriu; 17.07.2015
comment
@Bakuriu Это менее чисто функциональные языки, потому что нет языковой поддержки для написания чистого кода. Они все же позволяют писать чисто функциональный код; На самом деле это то, как написаны значительные части кода. - person Cubic; 17.07.2015
comment
@Cubic ‹serious› В самом деле. ‹/Serious› ‹flippant› C также позволяет писать чисто функциональный код. ‹/Flippant› ‹admittedly› Здесь не так много чисто функционального C. ‹/Admittedly› - person AndrewC; 20.07.2015

Строго говоря, это утверждение неверно, потому что в 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 мог бы решить его аналогичным образом, если бы чистота в любом случае не считалась желательной (ограничение мономорфизма в любом случае является почти решением, вам просто нужно запретить людям работать над этим путем используя подпись типа, как я сделал выше).

person Jonathan Cast    schedule 17.07.2015
comment
Большинство применений unsafePerformIO, которые я видел, даже не для ввода-вывода, а для ситуаций, когда ST не совсем подходит (например: параллельные алгоритмы, которые всегда возвращают один и тот же результат при тех же аргументах, но которые нельзя доказать. для этого просто используя систему типов) - person Jeremy List; 20.07.2015