Все мы знаем (или должны знать), что Haskell по умолчанию ленив. Ничего не оценивается до тех пор, пока не будет оценено. Итак, когда нужно что-то оценивать? Есть моменты, в которых Haskell должен быть строгим. Я называю это «точками строгости», хотя этот конкретный термин не так широко распространен, как я думал. По мне:
Редукция (или оценка) в Haskell происходит только в точках строгости.
Возникает вопрос: в чем конкретно заключаются точки строгости Haskell? Моя интуиция говорит , что main
, seq
/ челка модель, сопоставление с образцом, и любое IO
действие , совершаемое с помощью main
являются первичными точками Строгости, но я не знаю , почему я это знаю.
(Кроме того , если они не называются «точка строгости», что есть они называются?)
Думаю, хороший ответ будет включать обсуждение WHNF и так далее. Я также предполагаю, что это может коснуться лямбда-исчисления.
Изменить: дополнительные мысли по этому вопросу.
Размышляя над этим вопросом, я думаю, было бы яснее добавить что-нибудь к определению точки строгости. Пункты строгости могут иметь различный контекст и различную глубину (или строгость). Возвращаясь к моему определению, что «сокращение в Haskell происходит только в точках строгости», давайте добавим к этому определению следующее предложение: «точка строгости срабатывает только тогда, когда оценивается или уменьшается окружающий его контекст».
Итак, позвольте мне попытаться начать с того ответа, который я хочу. main
это пункт строгости. Это специально обозначено как первичная точка строгости своего контекста: программа. Когда программа ( main
контекст) оценивается, точка строгости main активируется. Глубина Main максимальна: ее нужно полностью оценить. Main обычно состоит из действий ввода-вывода, которые также являются точками строгости, контекст которых есть main
.
Теперь вы попробуете: обсудить seq
и сопоставить с образцом в этих терминах. Разъясните нюансы применения функции: насколько строго? Как это не так? О чем deepseq
? let
а case
заявления? unsafePerformIO
? Debug.Trace
? Определения верхнего уровня? Строгие типы данных? Узоры взрыва? И т. Д. Сколько из этих элементов можно описать в терминах простой последовательности или сопоставления с образцом?
источник
seq
и сопоставления с образцом достаточно, а все остальное определяется в их терминах. Я думаю, что сопоставление с образцом, например, обеспечивает жесткостьIO
действий.+
встроенные числовые типы, также требуют строгости, и я предполагаю, что то же самое применимо к чистым вызовам FFI.Ответы:
Хорошее место для начала - понимание этой статьи: Естественная семантика ленивого вычисления (Лаанчбери). Это подскажет вам, когда выражения оцениваются для небольшого языка, подобного GHC Core. Затем остается вопрос, как полностью отобразить Haskell на Core, и большая часть этого перевода дается самим отчетом Haskell. В GHC мы называем этот процесс «обессахариванием», потому что он удаляет синтаксический сахар.
Что ж, это еще не все, потому что GHC включает в себя целый ряд оптимизаций между обессахариванием и генерацией кода, и многие из этих преобразований перестроят ядро, так что результаты будут оцениваться в разное время (анализ строгости, в частности, заставит вещи оцениваться ранее). Итак, чтобы действительно понять, как будет оцениваться ваша программа, вам нужно взглянуть на Ядро, созданное GHC.
Возможно, этот ответ кажется вам немного абстрактным (я специально не упоминал шаблоны взрыва или последовательность), но вы просили что-то точное , и это лучшее, что мы можем сделать.
источник
Я бы, вероятно, переформулировал этот вопрос так: при каких обстоятельствах Haskell будет оценивать выражение? (Возможно, закрепите «нормальную форму для слабой головы».)
В первом приближении это можно уточнить следующим образом:
Из вашего интуитивно понятного списка основные действия и действия ввода-вывода попадают в первую категорию, а последовательность операций и сопоставление с образцом - во вторую категорию. Но я думаю, что первая категория больше соответствует вашей идее о «точке строгости», потому что именно так мы заставляем оценку в Haskell стать наблюдаемым эффектом для пользователей.
Предоставление всех деталей в частности - большая задача, поскольку Haskell - это большой язык. Это также довольно тонко, потому что Concurrent Haskell может оценивать вещи умозрительно, даже если мы в конечном итоге не используем результат: это третья разновидность вещей, вызывающих оценку. Вторая категория достаточно хорошо изучена: вы хотите посмотреть на строгость задействованных функций. Первая категория тоже можно рассматривать как своего рода «строгость», хотя это немного хитроумный , потому что
evaluate x
иseq x $ return ()
на самом деле это разные вещи! Вы можете относиться к этому должным образом, если дадите какую-то семантику монаде ввода-вывода (явная передачаRealWorld#
токена работает для простых случаев), но я не знаю, есть ли вообще название для такого рода стратифицированного анализа строгости.источник
C имеет концепцию точек последовательности , которые гарантируют для определенных операций, что один операнд будет оцениваться раньше другого. Я думаю, что это наиболее близкая из существующих концепций, но по существу эквивалентный термин точка строгости (или, возможно, точка силы ) больше соответствует мышлению Haskell.
Итак, вы думаете о
!
/$!
иseq
по сути правильно, но сопоставление с образцом подчиняется более тонким правилам.~
Конечно, вы всегда можете использовать ленивое сопоставление с образцом. Интересный момент из той же статьи:Давайте продолжим изучение кроличьей норы и посмотрим на документацию по оптимизации, выполненную GHC:
Другими словами, строгий код может быть сгенерирован где угодно в качестве оптимизации, потому что создание переходов является излишне дорогостоящим, когда данные всегда будут нужны (и / или могут использоваться только один раз).
(Термин находится в нормальной форме заголовка, если в позиции заголовка 1 нет бета-редекса . Редекс - это редекс заголовка, если ему предшествуют только лямбда-абстракторы нередексов 2. ) Итак, когда вы начинаете форсировать преобразователь, вы работаете в WHNF; когда больше не нужно форсировать thunks, вы в нормальной форме. Еще один интересный момент:
Это, естественно, подразумевает, что на самом деле любое
IO
действие, выполняемое из,main
вызывает принудительную оценку, что должно быть очевидным, учитывая, что программы на Haskell действительно что-то делают. Все, что должно пройти через последовательность, определенную в,main
должно быть в нормальной форме и поэтому подлежит строгой оценке.Однако CA McCann правильно понял это в комментариях: единственное, что особенного в
main
этом,main
- это то, что определено как особенное; сопоставления с образцом в конструкторе достаточно для обеспечения последовательности, налагаемойIO
монадой. Только в этом отношенииseq
и сопоставление с образцом являются фундаментальными.источник
Show
экземпляр печатаемого значения.Haskell - это, AFAIK, не чистый ленивый язык, а скорее нестрогий язык. Это означает, что он не обязательно оценивает сроки в последний возможный момент.
Хороший источник модели «лени» haskell можно найти здесь: http://en.wikibooks.org/wiki/Haskell/Laziness.
По сути, важно понимать разницу между преобразователем и нормальной формой слабого заголовка WHNF.
Насколько я понимаю, haskell выполняет вычисления в обратном порядке по сравнению с императивными языками. Это означает, что при отсутствии шаблонов "seq" и bang это, в конечном счете, будет побочным эффектом, который вынуждает оценивать преобразователь, что, в свою очередь, может вызвать предварительные оценки (истинная лень).
Поскольку это привело бы к ужасной утечке места, компилятор заранее выясняет, как и когда оценивать преобразователи, чтобы сэкономить место. Затем программист может поддержать этот процесс, предоставив аннотации строгости (en.wikibooks.org/wiki/Haskell/Strictness, www.haskell.org/haskellwiki/Performance/Strictness) для дальнейшего уменьшения использования пространства в виде вложенных преобразователей.
Я не разбираюсь в операционной семантике haskell, поэтому оставлю ссылку в качестве ресурса.
Еще несколько ресурсов:
http://www.haskell.org/haskellwiki/Performance/Laziness
http://www.haskell.org/haskellwiki/Haskell/Lazy_Evaluation
источник
Ленивый не означает ничего не делать. Каждый раз, когда ваш программный шаблон соответствует
case
выражению, он что-то оценивает - в любом случае достаточно. В противном случае он не сможет определить, какой RHS использовать. Не видите в коде никаких case-выражений? Не волнуйтесь, компилятор переводит ваш код в урезанную форму Haskell, где их трудно избежать.Основное практическое правило для новичков
let
- лениться,case
меньше лениться.источник
case
всегда принудительно выполняет оценку в GHC Core, в обычном Haskell этого не происходит. Например, попробуйтеcase undefined of _ -> 42
.case
в GHC Core оценивает свой аргумент для WHNF, в то время какcase
в Haskell оценивает свой аргумент столько, сколько необходимо для выбора соответствующей ветви. В примере с hammar это совсем не так, но incase 1:undefined of x:y:z -> 42
оценивает глубже, чем WHNF.case something of (y,x) -> (x,y)
вообще не нужно оцениватьsomething
. Это верно для всех типов продуктов.something
должен быть оценен как WHNF, чтобы достичь конструктора кортежа.Это не полный ответ, направленный на карму, а всего лишь часть головоломки - поскольку речь идет о семантике, имейте в виду, что существует несколько стратегий оценки, которые обеспечивают одинаковую семантику. Одним из хороших примеров здесь - и этот проект также говорит о том, как мы обычно думаем о семантике Haskell, - был проект Eager Haskell, который радикально изменил стратегии оценки при сохранении той же семантики: http://csg.csail.mit.edu/ pubs / haskell.html
источник
Компилятор Glasgow Haskell переводит ваш код на язык, похожий на лямбда-исчисление, который называется core . В этом языке что-то будет оцениваться всякий раз, когда вы сопоставляете его с шаблоном с помощью
case
оператора. Таким образом, если функция вызывается, будет оцениваться самый внешний конструктор и только он (если нет принудительных полей). Все остальное консервируется в преобразователе. (Преобразователи вводятсяlet
привязками).Конечно, это не совсем то, что происходит на реальном языке. Компилятор преобразует Haskell в Core очень изощренным способом, делая как можно больше вещей ленивыми, а все, что всегда нужно, - ленивым. Кроме того, существуют неупакованные значения и кортежи, которые всегда строги.
Если вы попытаетесь оценить функцию вручную, вы можете подумать:
источник
Нет.
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
в этом плане нет ничего особенного.Снижение происходит везде
Я не понимаю, как понять это заявление. Каждая часть программы имеет какое-то значение, и это значение определяется правилами сокращения, поэтому сокращение происходит везде.
Для уменьшения функции приложения
<expr1> <expr2>
, вы должны оценить ,<expr1>
пока он не имеет формы , как(\x -> <expr1'>)
и(getChar >>=)
или что - то другое , что соответствует правилу. Но по какой-то причине приложение функции не появляется в списках выражений, которые якобы "вынуждают вычислять", хотя этоcase
всегда происходит.Вы можете увидеть это заблуждение в цитате из вики-страницы Haskell, найденной в другом ответе:
Я не понимаю, что можно квалифицировать как «чисто ленивый язык» для того, кто это написал, кроме, возможно, языка, на котором каждая программа зависает, потому что среда выполнения ничего не делает. Если сопоставление с образцом является особенностью вашего языка, тогда вам действительно нужно это сделать в какой-то момент. Для этого вы должны достаточно оценить проверяемого, чтобы определить, соответствует ли он шаблону. Это самый ленивый способ сопоставить шаблон, который в принципе возможен.
~
-префиксные шаблоны программисты часто называют «ленивыми», но в спецификации языка они называются «неопровержимыми». Их определяющее свойство - они всегда совпадают. Поскольку они всегда совпадают, вам не нужно оценивать проверяемого, чтобы определить, совпадают они или нет, поэтому ленивая реализация не будет. Разница между обычными и неопровержимыми шаблонами заключается в том, какие выражения они соответствуют, а не в том, какую стратегию оценки вы должны использовать. В спецификации ничего не говорится о стратегиях оценки.Я не уверен, что все это имеет какое-то значение.
Нет,
main
нужно только оценивать «неглубоко», чтобы действия ввода-вывода появлялись на верхнем уровне.main
- это вся программа, и программа не полностью оценивается при каждом запуске, потому что не весь код имеет отношение к каждому запуску (в целом).Я уже говорил о сопоставлении с образцом.
seq
могут быть определены правилами, аналогичнымиcase
и application: например,(\x -> <expr1>) `seq` <expr2>
сокращается до<expr2>
. Это «принудительно оценивает» так же, какcase
и приложение. WHNF - это просто название того, к чему эти выражения «принудительно вычисляют».Он строг в левом выражении, как
case
строг в своем внимании. Он также строг в теле функции после замены, так же какcase
строг в правой части выбранной альтернативы после замены.Это просто библиотечная функция, а не встроенная функция.
Кстати,
deepseq
семантически странно. Следует привести только один аргумент. Я думаю, что тот, кто это придумал, просто слепо скопировал,seq
не понимая, зачемseq
нужны два аргумента. Я считаюdeepseq
имя и спецификацию доказательством того, что плохое понимание оценки Haskell является обычным явлением даже среди опытных программистов на Haskell.Я говорил о
case
.let
после удаления сахара и проверки типа - это просто способ записи графа произвольного выражения в виде дерева. Вот статья об этом .В некоторой степени это может быть определено правилами редукции. Например,
case unsafePerformIO <expr> of <alts>
сокращается доunsafePerformIO (<expr> >>= \x -> return (case x of <alts>))
и только на верхнем уровнеunsafePerformIO <expr>
сокращается до<expr>
.Это не делает мемоизации. Вы можете попытаться имитировать мемоизацию, переписав каждое
unsafePerformIO
выражение так, чтобы оно явным образом запомнилось, и создавIORef
где-нибудь связанный s ... Но вы никогда не сможете воспроизвести поведение мемоизации GHC, потому что оно зависит от непредсказуемых деталей процесса оптимизации и потому, что оно даже небезопасно по типу (как показывает печально известный полиморфныйIORef
пример в документации GHC).Debug.Trace.trace
просто обертка вокругunsafePerformIO
.Привязки переменных верхнего уровня аналогичны вложенным
let
привязкам.data
,class
,import
, И такие совершенно разные игры с мячом.Просто сахар для
seq
.источник