Что такое строгость Хаскелла?

Мы все знаем (или должны знать), что по умолчанию Haskell ленив. Ничто не оценивается, пока оно не будет оценено. Итак, когда нужно что-то оценивать? Есть точки, где Хаскелл должен быть строгим. Я называю эти "точки строгости", хотя этот конкретный термин не так широко распространен, как я думал. По мне:

Сокращение (или оценка) в Haskell происходит только в точках строгости.

Итак, вопрос: , что именно, являются точками строгости Хаскелла?. Моя интуиция говорит, что main, seq/bang-шаблоны, сопоставление образцов и любое действие IO, выполняемое через main являются основными точками строгости, но я действительно не знаю, почему я это знаю.

(Кроме того, если они не называются "точками строгости", что они называются?)

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


Изменить: дополнительные мысли по этому вопросу.

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

Итак, позвольте мне попытаться начать с того типа ответа, который я хочу. main - точка строгости. Он специально обозначается как основная точка строгости его контекста: программа. Когда оценивается программа (main контекст), активируется точка строгости main. Основная глубина максимальна: она должна быть полностью оценена. Основной обычно состоит из операций ввода-вывода, которые также являются точками строгости, контекст которых main.

Теперь попробуйте: обсудите seq и сопоставление шаблонов в этих условиях. Объясните нюансы применения функций: как это строго? Как это не так? Что насчет deepseq? let и case заявления? unsafePerformIO? Debug.Trace? Определения верхнего уровня? Строгие типы данных? Модели взлома? И т.д. Сколько из этих элементов можно описать с точки зрения просто seq или соответствия шаблонов?

Ответ 1

Хорошим местом для начала является понимание этой статьи: Естественная семантика для ленивой оценки (Launchbury). Это скажет вам, когда выражения оцениваются для небольшого языка, подобного GHC Core. Тогда остается вопрос, как отобразить полный Haskell в Core, и большая часть этого перевода дается самим докладом Haskell. В GHC мы называем этот процесс "desugaring", потому что он удаляет синтаксический сахар.

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

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

Ответ 2

Я бы, вероятно, передумал этот вопрос: "При каких обстоятельствах Хаскелл оценивает выражение? (Возможно, придерживайтесь" на слабой голове нормальной форме".)

В первом приближении мы можем указать это следующим образом:

  • Выполнение операций ввода-вывода будет оценивать любые выражения, которые им "нужны". (Таким образом, вам нужно знать, выполняется ли действие ввода-вывода, например, это имя является основным, или оно вызывается из основного И вам нужно знать, что нужно для действия).
  • Выражение, которое оценивается (эй, что рекурсивное определение!) будет оценивать любые выражения, которые ему нужны.

Из вашего интуитивного списка основные и IO-действия попадают в первую категорию, а seq и сопоставление шаблонов попадают во вторую категорию. Но я думаю, что первая категория больше соответствует вашей идее "точки строгости", потому что на самом деле мы делаем оценку в Haskell, чтобы стать наблюдаемыми эффектами для пользователей.

Предоставление всех деталей специально является большой задачей, поскольку Haskell - это большой язык. Это также довольно тонко, потому что Concurrent Haskell может оценивать вещи спекулятивно, хотя в конечном итоге мы не используем результат: это третья порода вещей, которые вызывают оценку. Вторая категория довольно хорошо изучена: вы хотите посмотреть на строгость задействованных функций. Первая категория тоже может считаться своего рода "строгостью", хотя это немного изворотливо, потому что evaluate x и seq x $ return () на самом деле разные вещи! Вы можете относиться к нему правильно, если вы даете какую-то семантику монаде IO (явно передавая токен RealWorld# для простых случаев), но я не знаю, есть ли имя для такого рода стратифицированного анализа строгости в целом.

Ответ 3

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

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

& hellip;

Программисты также могут использовать примитив seq, чтобы заставить выражение оценивать независимо от того, будет ли результат использоваться когда-либо.

$! определяется в терминах seq.

Lazy vs. non-strict.

Итак, ваше мышление о !/$! и seq является, по сути, правильным, но соответствие шаблонов подчиняется более тонким правилам. Конечно, вы всегда можете использовать ~ для ленивого совпадения шаблонов. Интересный момент из той же статьи:

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

Продолжайте движение по кроличьей дыре и посмотрите на документы для оптимизаций, выполняемых GHC:

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

Оптимизация GHC: Анализ стресса.

Другими словами, строгий код может быть сгенерирован везде как оптимизация, потому что создание thunks неоправданно дорого, когда данные всегда понадобятся (и/или могут использоваться только один раз).

& hellip; больше не может быть проведена оценка стоимости; говорят, что она находится в нормальной форме. Если мы находимся на каком-либо промежуточном этапе, так что мы выполнили хотя бы некоторую оценку значения, она находится в слабой форме головы (WHNF). (Существует также "нормальная форма головы", но она не используется в Haskell.) Полная оценка чего-то в WHNF сводит его к чему-то в нормальной форме & hellip;

Wikibooks Haskell: Laziness

(член находится в нормальной форме головы, если нет бета-redex в позиции головы 1. Redex является головным redex, если ему предшествуют только лямбда-абстракторы non-redexes 2.) Поэтому, когда вы начинаете форсировать удар, вы работаете в WHNF; когда больше нет толчков, оставшихся в силе, вы в нормальной форме. Еще один интересный момент:

& hellip; если в какой-то момент нам нужно было, скажем, распечатать z пользователю, нам нужно было бы полностью оценить его & hellip;

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

С. Однако А. Макканн понял это в комментариях: единственная особенность main заключается в том, что main определяется как специальный; сопоставление шаблонов на конструкторе достаточно для обеспечения последовательности, налагаемой монадой IO. В этом отношении только seq и сопоставление шаблонов являются фундаментальными.

Ответ 4

Haskell - это AFAIK не чистый ленивый язык, а скорее нестрогий язык. Это означает, что он не обязательно оценивает термины в последний возможный момент.

Хороший источник для модели "lazyness" haskell можно найти здесь: http://en.wikibooks.org/wiki/Haskell/Laziness

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

Мое понимание состоит в том, что haskell тянет вычисления в обратном направлении по сравнению с императивными языками. Это означает, что в отсутствие "seq" и ​​ "bang patterns" это в конечном счете будет каким-то побочным эффектом, который заставляет оценивать удар, который может повлечь за собой предварительные оценки (истинная ленивость).

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

Ответ 5

Lazy не означает ничего не делать. Всякий раз, когда ваш шаблон программы соответствует выражению case, он оценивает что-то - в любом случае. В противном случае он не может понять, какой RHS использовать. Не видите ли выражения в коде? Не волнуйтесь, компилятор переводит ваш код в урезанную форму Haskell, где их трудно избежать.

Для новичков основное правило: let - ленивое, case менее ленивое.

Ответ 6

Это не полный ответ, направленный на карму, а лишь часть головоломки - в той мере, в какой это касается семантики, помните, что существует множество стратегий оценки, которые обеспечивают одну и ту же семантику. Один хороший пример здесь - и проект также говорит о том, как мы обычно относимся к семантике Haskell - это проект Eager Haskell, который радикально изменил стратегии оценки, сохранив при этом одну и ту же семантику: http://csg.csail.mit.edu/pubs/haskell.html

Ответ 7

Компилятор Glasgow Haskell переводит ваш код в Lambda-исчисляющий язык, называемый ядром. На этом языке что-то будет оцениваться, когда вы сопоставляете его с помощью case -statement. Таким образом, если вызывается функция, то внешний конструктор и только он (если нет принудительных полей) будут оцениваться. Все остальное консервировано в куске. (Thunks вводятся привязками let).

Конечно, это не совсем то, что происходит на реальном языке. Компилятор конвертирует Haskell в Core очень сложным способом, создавая как можно больше лени и все, что всегда нужно ленивым. Кроме того, есть незагруженные значения и кортежи, которые всегда строги.

Если вы попытаетесь оценить функцию вручную, вы можете в принципе подумать:

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