Всегда ли функция в Haskell оценивает ее возвращаемое значение?
Я пытаюсь лучше понять Haskell лень, например, когда он оценивает аргумент функции.
Из этого source:
Но когда вычисляется вызов const
(здесь нас интересует, здесь, в конце концов), его возвращаемое значение также оценивается... Это хороший общий принцип: функция, очевидно, является строгой в его возвращаемое значение, потому что, когда приложение функции нужно оценивать, ему необходимо оценить, в теле функции, что возвращается. Начиная с этого момента вы можете знать, что должно оцениваться, глядя на то, что возвращаемое значение зависит от неизменно. Ваша функция будет строгой в этих аргументах и ленив в других.
Итак, функция в Haskell всегда оценивает собственное возвращаемое значение? Если у меня есть:
foo :: Num a => [a] -> [a]
foo [] = []
foo (_:xs) = map (* 2) xs
head (foo [1..]) -- = 4
В соответствии с вышеприведенным абзацем map (* 2) xs
необходимо оценить. Интуитивно я думаю, что это означает применение map
ко всему списку, приводящее к бесконечному циклу.
Но я могу успешно занять голову результата. Я знаю, что :
ленив в Haskell, значит ли это, что оценка map (* 2) xs
просто означает создание чего-то еще, еще не полностью оцененного?
Что значит оценивать функцию, примененную к бесконечному списку? Если возвращаемое значение функции всегда оценивается, когда функция оценивается, может ли функция когда-либо фактически возвращать thunk?
Edit:
bar x y = x
var = bar (product [1..]) 1
Этот код не зависает. Когда я создаю var
, не оценивает ли его тело? Или он устанавливает bar
в product [1..]
и не оценивает это? Если последний, bar
не возвращает свое тело в WHNF, правильно, так это действительно "оценило" x? Как bar
быть строгим в x
, если он не зависает при вычислении product [1..]
?
Ответы
Ответ 1
Прежде всего, Haskell не указывает, когда происходит оценка, поэтому для конкретных реализаций может быть задан определенный ответ.
Следующее верно для всех непараллельных реализаций, о которых я знаю, таких как ghc, hbc, nhc, hugs и т.д. (все основанные на G-машине, btw).
Кстати, нужно помнить, что когда вы слышите "оценку" для Haskell, это обычно означает "оценить WHNF".
В отличие от строгих языков, вы должны различать два "вызывающих" функции, первая - это то, где вызов происходит лексически, а второй - где требуется значение. Для строгого языка эти два всегда совпадают, но не для ленивого языка.
Давайте рассмотрим ваш пример и немного его усложним:
foo [] = []
foo (_:xs) = map (* 2) xs
bar x = (foo [1..], x)
main = print (head (fst (bar 42)))
Функция foo
встречается в bar
. Оценка bar
вернет пару, и первый компонент пары будет thunk, соответствующий foo [1..]
. Таким образом, bar
- это то, что будет вызывающим в строгом языке, но в случае ленивого языка он вообще не вызывает foo
, а просто строит закрытие.
Теперь в функции main
нам действительно нужно значение head (fst (bar 42))
, так как мы должны его распечатать. Таким образом, функция head
будет называться. Функция head
определяется путем сопоставления шаблонов, поэтому для нее требуется значение аргумента. Так называется fst
. Он также определяется путем сопоставления шаблонов и нуждается в его аргументе, поэтому вызывается bar
, а bar
возвращает пару, а fst
будет оценивать и возвращать свой первый компонент. И теперь наконец foo
"называется"; и по названию я имею в виду, что thunk оценивается (вводится так, как его иногда называют терминологией TIM), потому что это значение необходимо. Единственная причина, по которой вызывается фактический код для foo
, - это то, что мы хотим получить значение. Поэтому foo
лучше вернуть значение (т.е. WHNF). Функция foo
будет оценивать свой аргумент и заканчиваться во второй ветки. Здесь он будет заходить в код для map
. Функция map
определяется совпадением шаблонов и оценивает ее аргумент, что является минусом. Таким образом, карта вернет следующий {(*2) y} : {map (*2) ys}
, где я использовал {}
, чтобы указать построение замыкания. Так как вы можете видеть, что map
просто возвращает ячейку cons, при этом головка является закрывающейся, а хвост является замыканием.
Чтобы лучше понять эксплуатационную семантику Haskell, я предлагаю вам посмотреть на статью, в которой описывается, как перевести Haskell на некоторую абстрактную машину, такую как G-машина.
Ответ 2
Я всегда обнаружил, что термин "оценка", который я узнал в других контекстах (например, программирование схемы), всегда меня путал, когда я пытался применить его к Haskell, и что я сделал прорыв, когда начал подумать о Haskell в терминах выражений форсирования, а не "оценивать" их. Некоторые ключевые отличия:
- "Оценка", как я узнал ранее, термин синтаксически обозначает отображение выражений для значений, которые сами по себе не являются выражениями. (Один общий технический термин здесь - "обозначения".)
- В Haskell процесс форсирования - это ИМХО, наиболее легко понимаемый как переписывание выражений. Вы начинаете с выражения, и вы повторно переписываете его в соответствии с определенными правилами, пока не получите эквивалентное выражение, удовлетворяющее определенному свойству.
В Haskell "определенное свойство" имеет недружественное имя "слабая головная нормальная форма" ( "WHNF" ), что на самом деле просто означает, что выражение является либо нулевым конструктором данных, либо приложением конструктора данных.
Позвольте перевести это на очень грубый набор неофициальных правил. Чтобы заставить выражение expr
:
- Если
expr
- это нулевой конструктор или приложение-конструктор, результатом его принудительного выполнения является expr
. (Это уже в WHNF.)
- Если
expr
- это приложение функции f arg
, то результат принудительного его получения получается таким образом:
- Найдите определение
f
.
- Можете ли вы сопоставить это определение с выражением
arg
? Если нет, то сила arg
и повторите попытку с результатом.
- Замените переменные совпадения шаблонов в теле
f
с частями (возможно, переписанными) arg
, которые соответствуют им, и заставьте полученное выражение.
Один из способов думать об этом заключается в том, что когда вы принудительно выражаете выражение, вы пытаетесь переписать его минимально, чтобы уменьшить его до эквивалентного выражения в WHNF.
Пусть это применимо к вашему примеру:
foo :: Num a => [a] -> [a]
foo [] = []
foo (_:xs) = map (* 2) xs
-- We want to force this expression:
head (foo [1..])
Нам понадобятся определения для head
и `map:
head [] = undefined
head (x:_) = x
map _ [] = []
map f (x:xs) = f x : map f x
-- Not real code, but a rule we'll be using for forcing infinite ranges.
[n..] ==> n : [(n+1)..]
Итак, теперь:
head (foo [1..]) ==> head (map (*2) [1..]) -- using the definition of foo
==> head (map (*2) (1 : [2..])) -- using the forcing rule for [n..]
==> head (1*2 : map (*2) [2..]) -- using the definition of map
==> 1*2 -- using the definition of head
==> 2 -- using the definition of *
Ответ 3
Я считаю, что идея должна заключаться в том, что на ленивом языке, если вы оцениваете приложение-функцию, это должно быть потому, что вам нужен результат приложения для чего-то. Поэтому любая причина, по которой приложение функции будет уменьшено в первую очередь, будет продолжать уменьшать возвращаемый результат. Если бы нам не нужен результат функции, мы бы не оценивали этот вызов в первую очередь, все приложение оставалось бы как thunk.
Ключевым моментом является то, что стандартный заказ "ленивой оценки" управляется спросом. Вы оцениваете только то, что вам нужно. Оценка большего количества рисков, нарушающих определение спецификации языка "нестрогая семантика" и зацикливание или сбой для некоторых программ, которые должны быть в состоянии завершить; ленивая оценка имеет интересное свойство, которое, если какой-либо оценочный порядок может привести к завершению конкретной программы, поэтому может лениться оценить. 1
Но если мы оцениваем только то, что нам нужно, что значит "нужно"? Как правило, это означает, что
- соответствие шаблона должно знать, какой конструктор имеет конкретное значение (например, я не могу знать, какую ветвь взять в определении
foo
, не зная, является ли аргумент []
или _:xs
)
- примитивная операция должна знать все значение (например, арифметические схемы в ЦП не могут добавлять или сравнивать громы, мне нужно полностью оценить два значения
Int
для вызова таких операций)
- внешний драйвер, выполняющий действие IO
main
, должен знать, что следующая вещь для выполнения:
Скажем, у нас есть эта программа:
foo :: Num a => [a] -> [a]
foo [] = []
foo (_:xs) = map (* 2) xs
main :: IO ()
main = print (head (foo [1..]))
Чтобы выполнить main
, драйвер IO должен оценить thunk print (head (foo [1..]))
, чтобы выработать, что он print
применяется к thunk head (foo [1..])
. print
должен оценить свой аргумент для его печати, поэтому теперь нам нужно оценить этот файл.
head
начинается с шаблона, сопоставляющего его аргумент, поэтому теперь нам нужно оценить foo [1..]
, , но только для WHNF - достаточно, чтобы определить, является ли внешний конструктор списка []
или :
.
foo
начинается с сопоставления шаблонов в его аргументе. Поэтому нам нужно оценить [1..]
, также только для WHNF. Это в основном 1 : [2..]
, что достаточно, чтобы увидеть, какую ветвь взять в foo
. 2
Событие :
foo
(с xs
, привязанным к thunk [2..]
), оценивается с помощью thunk map (*2) [2..]
.
Итак, foo
оценивается и не оценивает его тело. Однако мы это сделали, потому что head
было сопоставлением с образцом, чтобы увидеть, были ли у нас []
или x : _
. Мы все еще не знаем этого, поэтому мы должны немедленно продолжить оценивать результат foo
.
Этот - это то, что означает статья, когда он говорит, что функции строгие в их результате. Учитывая, что вызов foo
вообще оценивается, его результат также будет оцениваться (и, следовательно, все, что необходимо для оценки результата, также будет оценено).
Но насколько он должен оцениваться, зависит от вызывающего контекста. head
- это только совпадение шаблонов по результату foo
, поэтому для WHFF требуется только результат. Мы можем получить бесконечный список для WHNF (мы уже это сделали, с 1 : [2..]
), поэтому мы не обязательно получаем бесконечный цикл при оценке вызова foo
. Но если head
была какой-то примитивной операцией, реализованной вне Haskell, которая должна была быть передана полностью оцененным списком, тогда мы полностью оценили бы foo [1..]
и, таким образом, никогда не закончили бы, чтобы вернуться к head
.
Итак, просто для завершения моего примера мы оцениваем map (2 *) [2..]
.
map
шаблон соответствует его второму аргументу, поэтому нам нужно оценить [2..]
до 2 : [3..]
. Этого достаточно для map
, чтобы вернуть thunk (2 *) 2 : map (2 *) [3..]
, который находится в WHNF. Итак, мы можем, наконец, вернуться к head
.
head ((2 *) 2 : map (2 *) [3..])
не нужно проверять обе стороны :
, он просто должен знать, что есть один, чтобы он мог вернуть левую сторону. Таким образом, он просто возвращает неоценимый thunk (2 *) 2
.
Опять же, мы оценили только вызов head
, потому что print
нужно знать, каков его результат, поэтому, хотя head
не оценивает его результат, его результат всегда оценивается всякий раз, когда вызов head
есть.
(2 *) 2
принимает значение 4
, print
преобразует его в строку "4"
(через show
), и строка выводится на выход. Это было полное действие main
IO, поэтому программа выполнена.
1Реализации Haskell, такие как GHC, не всегда используют "стандартную ленивую оценку", а языковая спецификация не требует этого. Если компилятор может доказать, что что-то всегда будет необходимо, или не может быть циклом/ошибкой, тогда его можно будет оценить, даже если ленивая оценка (пока) не сделает этого. Это часто бывает быстрее, поэтому оптимизация GHC действительно делает это.
2 Я просматриваю несколько деталей здесь, например, print
имеет некоторую не примитивную реализацию, которую мы могли бы выполнить внутри и лениво оценить, и что [1..]
можно было бы дополнительно расширить к функциям, которые фактически реализуют этот синтаксис.
Ответ 4
Не обязательно. Haskell ленив, что означает, что он оценивает только тогда, когда ему это нужно. Это имеет некоторые интересные эффекты. Если взять приведенный ниже код, например:
-- File: lazinessTest.hs
(>?) :: a -> b -> b
a >? b = b
main = (putStrLn "Something") >? (putStrLn "Something else")
Это вывод программы:
$ ./lazinessTest
Something else
Это означает, что putStrLn "Something"
никогда не оценивается. Но он все еще передается функции, в виде "thunk". Эти "трюки" - это неоцененные значения, которые, вместо того, чтобы быть конкретными значениями, похожи на паттерн для вычисления стоимости. Вот как работает ленивость Haskell.
В нашем случае два "thunks" передаются на >?
, но только один передается, что означает, что только один оценивается в конце. Это также относится к const
, где второй аргумент может быть безопасно проигнорирован и поэтому никогда не вычисляется. Что касается map
, GHC достаточно умен, чтобы понять, что мы не заботимся о конце массива и только потрудились вычислить, в чем он нуждается, в вашем случае второй элемент исходного списка.
Однако лучше оставить мысли о лени для компилятора и сохранить кодировку, если только вы не имеете дело с IO, и в этом случае вы действительно должны думать об лени, потому что вы можете легко ошибиться, просто продемонстрировали.
lots и серия онлайн-статей о Haskell wiki, чтобы посмотреть, если вы хотите больше деталей.
Ответ 5
Функция может оценить тип возвращаемого значения:
head (x:_) = x
или исключение/ошибка:
head _ = error "Head: List is empty!"
или внизу (⊥)
a = a
b = last [1 ..]