Все мы знаем (или должны знать), что Haskell по умолчанию ленив. Ничего не оценивается до тех пор, пока не будет оценено.
Нет.
Haskell - не ленивый язык
Haskell - это язык, в котором порядок оценки не имеет значения, поскольку нет побочных эффектов.
Не совсем верно, что порядок оценки не имеет значения, потому что язык допускает бесконечные циклы. Если вы не будете осторожны, можно застрять в тупике, где вы навсегда оцениваете подвыражение, когда другой порядок оценки привел бы к завершению за конечное время. Так что точнее сказать:
- Реализации Haskell должны оценивать программу таким образом, чтобы она завершалась, если есть какой-либо порядок оценки, который завершается. Только в том случае, если каждый возможный порядок оценки не завершается, реализация может не завершиться.
Это по-прежнему оставляет реализациям огромную свободу в оценке программы.
Программа Haskell - это одно выражение, а именно let {
все привязки верхнего уровня.} in Main.main
. Оценка может пониматься как последовательность сокращающих (небольших) шагов, которые изменяют выражение (которое представляет текущее состояние выполняющейся программы).
Вы можете разделить шаги редукции на две категории: те, которые доказуемо необходимы (доказуемо будут частью любой завершающей последовательности сокращений), и те, которые нет. Вы можете расплывчато разделить доказуемо необходимые сокращения на две подкатегории: те, которые «очевидно» необходимы, и те, которые требуют некоторого нетривиального анализа, чтобы доказать их необходимость.
Выполнение только явно необходимых сокращений - это то, что называется «ленивым вычислением». Я не знаю, существовала ли когда-либо реализация Haskell с чисто ленивыми оценками. Объятия, возможно, были одним из них. GHC определенно нет.
GHC выполняет шаги сокращения во время компиляции, которые не являются доказуемо необходимыми; например, он заменит 1+2::Int
на, 3::Int
даже если не может доказать, что результат будет использован.
В некоторых случаях GHC может также выполнять необязательные сокращения во время выполнения. Например, при генерации кода для оценки f (x+y)
, если x
и y
имеют тип Int
и их значения будут известны во время выполнения, но f
не может быть доказано использование его аргумента, нет причин не вычислять x+y
перед вызовом f
. Он использует меньше места в куче и меньше места для кода и, вероятно, работает быстрее, даже если аргумент не используется. Однако я не знаю, действительно ли GHC использует такие возможности оптимизации.
GHC определенно выполняет этапы оценки во время выполнения, что доказано только в результате довольно сложного кросс-модульного анализа. Это чрезвычайно распространено и может составлять основную часть оценки реалистичных программ. Ленивая оценка - это последняя резервная стратегия оценки; это не то, что обычно бывает.
Была ветвь «оптимистической оценки» GHC, которая давала гораздо больше умозрительных оценок во время выполнения. От него отказались из-за его сложности и постоянного обслуживания, а не из-за того, что он плохо работал. Если бы Haskell был таким же популярным, как Python или C ++, я уверен, что были бы реализации с гораздо более сложными стратегиями оценки времени выполнения, поддерживаемыми крупными корпорациями. Неленивая оценка - это не изменение языка, это просто инженерная задача.
Снижение происходит за счет ввода-вывода верхнего уровня, и ничего больше
Вы можете моделировать взаимодействие с внешним миром с помощью специальных правил сокращения с побочными эффектами, например: «Если текущая программа имеет форму getChar >>= <expr>
, тогда получите символ из стандартного ввода и уменьшите программу, чтобы <expr>
применить к полученному вами персонажу».
Вся цель системы времени выполнения состоит в том, чтобы оценивать программу до тех пор, пока она не получит одну из этих форм побочного эффекта, затем выполнить побочный эффект, затем повторить, пока программа не примет некоторую форму, которая подразумевает завершение, например return ()
.
Других правил о том, что и когда сокращать, нет. Есть только правила того, что к чему может сводиться.
Например, единственные правила для if
выражений - это то, что if True then <expr1> else <expr2>
может быть уменьшено до <expr1>
, if False then <expr1> else <expr2>
может быть уменьшено до <expr2>
, и if <exc> then <expr1> else <expr2>
, где <exc>
является исключительным значением, может быть уменьшено до исключительного значения.
Если выражение, представляющее текущее состояние вашей программы, является if
выражением, у вас нет другого выбора, кроме как выполнять сокращения для условия до тех пор, пока оно не станет True
или False
или <exc>
, потому что это единственный способ избавиться от if
выражения и иметь любую надежду достичь состояние, соответствующее одному из правил ввода-вывода. Но спецификация языка не говорит вам об этом во многих словах.
Такого рода неявные ограничения порядка - единственный способ «принудительного» выполнения оценки. Это частый источник путаницы для новичков. Например, люди иногда пытаются стать foldl
строже, foldl (\x y -> x `seq` x+y)
вместо того чтобы писать foldl (+)
. Это не работает, и ничего подобного никогда не сработает, потому что никакое выражение не может вычислить само себя. Оценка может быть только «сверху». seq
в этом плане нет ничего особенного.
Снижение происходит везде
Редукция (или оценка) в Haskell происходит только в точках строгости. [...] Моя интуиция подсказывает, что главные элементы, шаблоны seq / bang, сопоставление с образцом и любое действие ввода-вывода, выполняемое через main, являются основными пунктами строгости [...].
Я не понимаю, как понять это заявление. Каждая часть программы имеет какое-то значение, и это значение определяется правилами сокращения, поэтому сокращение происходит везде.
Для уменьшения функции приложения <expr1> <expr2>
, вы должны оценить , <expr1>
пока он не имеет формы , как (\x -> <expr1'>)
и (getChar >>=)
или что - то другое , что соответствует правилу. Но по какой-то причине приложение функции не появляется в списках выражений, которые якобы "вынуждают вычислять", хотя это case
всегда происходит.
Вы можете увидеть это заблуждение в цитате из вики-страницы Haskell, найденной в другом ответе:
На практике Haskell - не просто ленивый язык: например, сопоставление с образцом обычно строгое
Я не понимаю, что можно квалифицировать как «чисто ленивый язык» для того, кто это написал, кроме, возможно, языка, на котором каждая программа зависает, потому что среда выполнения ничего не делает. Если сопоставление с образцом является особенностью вашего языка, тогда вам действительно нужно это сделать в какой-то момент. Для этого вы должны достаточно оценить проверяемого, чтобы определить, соответствует ли он шаблону. Это самый ленивый способ сопоставить шаблон, который в принципе возможен.
~
-префиксные шаблоны программисты часто называют «ленивыми», но в спецификации языка они называются «неопровержимыми». Их определяющее свойство - они всегда совпадают. Поскольку они всегда совпадают, вам не нужно оценивать проверяемого, чтобы определить, совпадают они или нет, поэтому ленивая реализация не будет. Разница между обычными и неопровержимыми шаблонами заключается в том, какие выражения они соответствуют, а не в том, какую стратегию оценки вы должны использовать. В спецификации ничего не говорится о стратегиях оценки.
main
это пункт строгости. Это специально обозначено как первичная точка строгости своего контекста: программа. Когда программа ( main
контекст) оценивается, точка строгости main активируется. [...] Main обычно состоит из действий ввода-вывода, которые также являются точками строгости, контекст которых есть main
.
Я не уверен, что все это имеет какое-то значение.
Глубина Main максимальна: ее нужно полностью оценить.
Нет, main
нужно только оценивать «неглубоко», чтобы действия ввода-вывода появлялись на верхнем уровне. main
- это вся программа, и программа не полностью оценивается при каждом запуске, потому что не весь код имеет отношение к каждому запуску (в целом).
обсудить seq
и сопоставление с образцом в этих условиях
Я уже говорил о сопоставлении с образцом. seq
могут быть определены правилами, аналогичными case
и application: например, (\x -> <expr1>) `seq` <expr2>
сокращается до <expr2>
. Это «принудительно оценивает» так же, как case
и приложение. WHNF - это просто название того, к чему эти выражения «принудительно вычисляют».
Разъясните нюансы применения функции: насколько строго? Как это не так?
Он строг в левом выражении, как case
строг в своем внимании. Он также строг в теле функции после замены, так же как case
строг в правой части выбранной альтернативы после замены.
О чем deepseq
?
Это просто библиотечная функция, а не встроенная функция.
Кстати, deepseq
семантически странно. Следует привести только один аргумент. Я думаю, что тот, кто это придумал, просто слепо скопировал, seq
не понимая, зачем seq
нужны два аргумента. Я считаю deepseq
имя и спецификацию доказательством того, что плохое понимание оценки Haskell является обычным явлением даже среди опытных программистов на Haskell.
let
а case
заявления?
Я говорил о case
. let
после удаления сахара и проверки типа - это просто способ записи графа произвольного выражения в виде дерева. Вот статья об этом .
unsafePerformIO
?
В некоторой степени это может быть определено правилами редукции. Например, case unsafePerformIO <expr> of <alts>
сокращается до unsafePerformIO (<expr> >>= \x -> return (case x of <alts>))
и только на верхнем уровне unsafePerformIO <expr>
сокращается до <expr>
.
Это не делает мемоизации. Вы можете попытаться имитировать мемоизацию, переписав каждое unsafePerformIO
выражение так, чтобы оно явным образом запомнилось, и создав IORef
где-нибудь связанный s ... Но вы никогда не сможете воспроизвести поведение мемоизации GHC, потому что оно зависит от непредсказуемых деталей процесса оптимизации и потому, что оно даже небезопасно по типу (как показывает печально известный полиморфный IORef
пример в документации GHC).
Debug.Trace
?
Debug.Trace.trace
просто обертка вокруг unsafePerformIO
.
Определения верхнего уровня?
Привязки переменных верхнего уровня аналогичны вложенным let
привязкам. data
, class
,import
, И такие совершенно разные игры с мячом.
Строгие типы данных? Узоры взрыва?
Просто сахар для seq
.
seq
и сопоставления с образцом достаточно, а все остальное определяется в их терминах. Я думаю, что сопоставление с образцом, например, обеспечивает жесткостьIO
действий.