Любопытно, как "loop = loop" оценивается в Haskell

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

Например, в GHCi эти выражения блокируются до я Control+C, но не потребляют процессор. Похоже, он спал.

let loop = loop
let loop = 1 + loop

Я попытался скомпилировать эти программы с помощью GHC:

main = print loop
  where loop = 1 + loop

main = print loop
  where loop = if True then loop else 1

Что было напечатано:

Main: <<loop>>

Итак, мой вопрос: Очевидно, что эти выражения скомпилированы в нечто отличное от циклов или рекурсивных вызовов на императивных языках. С чем они собрались? Является ли это специальным правилом для обработки функций 0-arg, которые имеют себя в правой части, или это частный случай чего-то более общего, что я не знаю?

[EDIT]:

Еще один вопрос: если это происходит из-за специальной обработки из компилятора, в чем причина этого, когда невозможно проверить все бесконечные циклы? "Знакомые" языки не заботятся о таких случаях, как while (true); или int f() { return f();}, правильно?

Большое спасибо.

Ответы

Ответ 1

GHC реализует Haskell как машину для уменьшения графа. Представьте свою программу в виде графика с каждым значением как node, а строки - от каждого значения, от которого зависит значение. Кроме того, мы ленивы, поэтому вы действительно начинаете только с одного node - и чтобы оценить, что node, GHC должен "enter" его и открыть его до функции с аргументами. Затем он заменяет вызов функции телом функции и пытается уменьшить ее достаточно, чтобы получить ее в обычную форму головы и т.д.

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

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

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

Таким образом, из-за использования черных дыр по нескольким причинам у нас есть некоторое понятие о отмеченном "пути", за которым последовала оценка. И если путь пересекает себя, мы можем сказать и выбросить исключение. Однако есть миллионы способов расхождения вещей, которые не связаны с самим пересечением путей. И в этих случаях мы не можем сказать и не вызывать исключения.

Для сверхъестественных технических подробностей о текущей реализации черных дыр см. доклад Саймона Марло из недавнего семинара исполнителей Haskell, "Планирование ленивой оценки на многоядерном" внизу http://haskell.org/haskellwiki/HaskellImplementorsWorkshop/2010.

Ответ 2

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

Что касается того, почему Haskell находит это чаще, чем другие языки:

  • Эти случаи не встречаются на таких строгих языках, как C. Эти циклы особенно происходят, когда вычисление ленивых переменных зависит от собственного значения.
  • Языки, такие как C, имеют очень специфическую семантику в циклах; т.е. какой порядок делать то, что внутри. Таким образом, они вынуждены фактически выполнять цикл. Однако Haskell определяет специальное значение _|_ ( "bottom" ), которое используется для представления ошибочных значений. Значения, которые строги сами по себе, т.е. Зависят от их собственного значения для вычисления, - это _|_. Результатом сопоставления шаблонов на _|_ может быть либо бесконечный цикл, либо исключение; ваш компилятор выбирает последний здесь.
  • Компилятор Haskell очень заинтересован в выполнении анализа строгости - то есть, доказывая, что определенное выражение зависит от некоторых других выражений - для выполнения определенных оптимизаций. Этот анализ цикла естественно выпадает как краевой случай в анализаторе строгости, который должен быть обработан так или иначе.