И 1, и "hello" находятся в нормальной форме, поэтому не являются преобразователями. Так почему же GHCI показывает, что "hello" — это преобразователь?
ghci> x = 1::Int
ghci> :sprint x
x = 1
ghci> x = "hello"::String
ghci> :sprint x
x = _
Редактировать: я копирую комментарий из моего предыдущего поста, который пытается объяснить этот вопрос.
Язык Haskell (или -standard) на самом деле не определяет, какое значение является преобразователем, что находится в нормальной форме и т. д.. Все, что он определяет, - это семантика строгости функций, которые вы можете вызывать для таких значений. В ваших примерах этого не происходит, поэтому в основном компилятор может делать что угодно, возможно, включая эвристики, которые для такого-то и такого-то типа преобразователь, вероятно, не даст ничего, кроме увеличения накладных расходов памяти.
Может ли кто-нибудь дать мне несколько статей или документов об этом?
что касается "hello", это то, что будет оцениваться как список, поскольку "hello" в конечном итоге оценивается как ('h':('e':('l':('l':('o':[]))))).
@WillemVanOnsem Результат тот же, независимо от того, включаете ли вы OverloadedStrings или нет. Я пишу :: String, чтобы было понятно, что это не связано с ограничением мономорфизма.





Если вы скажете GHC напечатать обесахаривание "hello" в GHC Core, вы увидите, что строковый литерал обессахаривается до вызова unpackCString#. Фактическое содержимое строки хранится компактно "hello"#, а функция unpackCString# строит соответствующий связанный список 'h' : 'e' : 'l' : 'l' : 'o' : [] по запросу, поэтому при потоковом использовании требуется очень мало памяти.
> :set -ddump-ds -dsuppress-all
> x = "hello"
==================== Desugared ====================
letrec {
x_aCa = letrec { x_aCe = unpackCString# "hello"#; } in x_aCe; } in
returnIO (: (unsafeCoerce# x_aCa) [])
Как я уже говорил ранее, это в основном связано с тем, как GHCi решает хранить значения, а не с тем, что указано в стандарте Haskell. Преобразователи/лень — это деталь реализации того, как достигается нестрогость, и иногда они также являются хорошей ментальной моделью, но не всегда. Строгость касается функций и того, как ⊥-значения могут или не могут распространяться. Любая мудрость о том, где преобразователь может или не может появиться, в основном является побочным продуктом этого.
Но есть еще пара вещей, о которых следует помнить на чистом уровне value/thunk.
Полиморфные значения всегда оцениваются по требованию; в основном это функции с невидимыми аргументами. В GHCi (в котором ограничение мономорфизма отключено по умолчанию) легко определить полиморфные значения без необходимости, в частности числовые литералы. Тогда :sprint всегда будет давать только _.
Это не проблема в ваших примерах: явное ограничение конкретным типом обеспечивает мономорфные привязки.
Пока вы напрямую используете простые конструкторы, есть большая вероятность, что компилятор/интерпретатор сохранит их напрямую в нормальной форме. Он делает это, например, для чего-то вроде ('a', (Just 'b','c')). Но я не думаю, что это когда-либо гарантировано, и обычно это в любом случае не имеет значения, потому что буквальные конструкторы только доходят до вас. Все, что гарантируется, — это то, что значение, определенное как внешний конструктор, может быть оценено как WHNF, не сталкиваясь с ⊥, но здесь мы снова находимся в области свойств строгости.
Как правило, значения, которые вы связываете, поступают из какой-то функции и, таким образом, начинаются как переходники. В частности, это относится и ко многим выражениям, которые выглядят как простые конструкторы, но таковыми не являются: большинство литералов в Haskell могут быть интеллектуальными литералами либо по умолчанию, либо через расширения. Наиболее известно, что числовые литералы неявно заключены в fromInteger, чтобы быть полиморфными для любого типа Num. Таким образом, это действительно немного удивительно, что
x = 1::Int
похоже, не производит никакого thunk. Но опять же: это зависит от компилятора, и это просто имеет смысл: нет никакого риска ⊥, значение прямо здесь, готовое к сохранению, тогда как построение санка вокруг него повлечет за собой совершенно ненужную косвенность указателя.
Стоит отметить, что это происходит не для всех числовых литералов:
ghci> let y = 1678796789876876987698768798769876986780987609876 :: Integer
ghci> :sprint y
y = _
В этом случае число больше не вписывается в 64-битное число, поэтому менее ясно, что размещение его прямо в памяти выгодно; оказывается, GHC не рискует в этом случае.
Теперь для строк у нас была бы почти такая же ситуация, если бы -XOverloadedStrings был активен, и в этом случае все строковые литералы должны были бы быть заключены в fromString . Но нет никакой гарантии, что наоборот: то, что расширение не активно, не означает, что GHC не может обернуть литерал какой-либо другой функцией. Как показал Ли-яо Ся, он оборачивает его в низкоуровневую функцию unpackCString#. (Это, безусловно, имеет смысл с точки зрения памяти, потому что строка C непрерывна и эффективна в памяти, тогда как строки связанного списка Haskell откровенно ужасны для хранения, занимая примерно в 16 раз больше памяти.) Это законно, пока компилятор убедитесь, что вы не столкнетесь с ⊥ при оценке строки.
В этом случае явное определение строки с помощью простых конструкторов приводит к ее немедленному сохранению в нормальной форме:
ghci> let x = "hello" :: String
ghci> :sprint x
x = _
ghci> let x' = 'h':'e':'l':'l':'o':[]
ghci> :sprint x'
x' = "hello"
Спасибо за подробный ответ. Итак, то, что говорится в викибуке, является просто ментальной моделью, а не тем, как GHC на самом деле реализует ленивую оценку. Но порядок или оценка по-прежнему верны, верно? Эти значения оцениваются по слоям: оценка исходит из самых внешних конструкторов данных выражений и работает внутрь.
Обычно да, но опять же компилятор может изменить порядок почти произвольно, если это будет сочтено выгодным в каком-то смысле. Пока это не меняет семантику строгости.
@TrungDo Нет. Факт: если вы продолжаете сокращать выражение, вы либо приходите к нормальной форме, либо застреваете в бесконечном цикле. Далее, любая последовательность редукций, дающая нормальную форму, дает ту же нормальную форму. Это означает, что GHC может выбрать для выражения любую стратегию вычисления, которая ему нравится, при условии, что он позаботится о том, чтобы избежать ненужных бесконечных циклов. Ленивое вычисление — это простая стратегия, которая гарантированно найдет нормальную форму, если она есть. Но если GHC знает лучше (скорее в оптимизированном скомпилированном коде, чем в GHCi), он может выполнять неленивые вычисления.
@HTNW Спасибо. Так что мне не следует заморачиваться с переходниками и WHNF, а просто нужно помнить о свойстве нестрогости: f(⊥) ≠ ⊥
@TrungDo Нет, WHNF по-прежнему важен. Все в моем предыдущем комментарии более или менее справедливо как для WHNF, так и для нормальной формы, а WHNF — это концепция, на которой основана идея строгости и остальная часть спецификации Haskell. Но думать о преобразователях действительно излишне, пока вам не нужно беспокоиться о производительности (или использовать unsafePerformIO).
@HTNW Спасибо. Теперь это имеет смысл для меня. К сожалению, каждый текст, который я читал, устанавливает сильную связь между преобразователями и WHNF.
почему вы используете
:: String, вы работаете с расширениемOverloadedStrings?