Чем отличается 'seq' в Haskell от других функций?
Я запутался в описании того, как Хаскелл seq
работает в учебнике, который я читаю.
Учебник утверждает, что
оценивая выражение
seq x y
сначала оценимx
в WHNF и только затем продолжить оценкуy
Но ранее в том же учебном пособии, в котором объясняется, как ленивая оценка Haskell работает в целом, говорится, что при оценке функции аргументы "оцениваются, но только по мере необходимости", что означает, что
аргументы будут оцениваться слева направо, пока их верхние узлы не станут конструкторами, соответствующими шаблону. Если шаблон - простая переменная, то аргумент не оценивается; если шаблон является конструктором, это означает оценку для WHNF.
Это описание оценки функции в целом, похоже, не отличается от описания, приведенного для seq
, Оба - в чтении моего новичка - просто сводят свой первый аргумент к WHNF.
Это верно? Как seq
отличается - особенно в том, как он обрабатывает свой первый аргумент - от любой другой функции Haskell?
2 ответа
Без seq
, evaluate
, шаблоны взрыва и т. д., применяется следующее правило:
Вся оценка определяется непосредственно сопоставлением с образцом, оценкой
if
условия или примитивные числовые операции.
Как оказалось, мы можем немного щуриться и сделать все это похожим на сопоставление с образцом:
if c then x else y
=
case c of
True -> x
False -> y
x > y = case x of
0 -> case y of ...
0 + y = y
1 + 1 = 2
и т.д. В конечном счете, вещь, которая оценивается в любое время, является следующим примитивом. IO
действие, которое предпримет программа, а все остальное просто рекурсивно управляется сопоставлением с образцом.
Слева направо означает, что, например,
foo False 'd' = ...
foo True _ = ...
эквивалентно
foo x y = case x of
False -> case y of
'd' -> ...
True -> ...
Так что если foo
применяется к True
и какое-то другое значение, оно не беспокоит форсирование этого значения, потому что сначала проверяет левый паттерн.
seq
применительно к данным действует как глупость case
, Если x :: Bool
, затем
x `seq` y = case x of
True -> y
False -> y
Но seq
может довольно просто применяться к функциям или к вещам неизвестного типа. Он предлагает способ заставить оценку вещей помимо обычной цепочки сопоставления с образцом.
В первые дни Хаскелла, seq
был метод Seq
класс, и это имело смысл. К сожалению, разработчики сочли раздражающим иметь дело с этим классом, когда проще было просто "обмануть" и сделать seq
работать на все. Таким образом, они обманули, и с тех пор некоторые аспекты анализа и трансформации программ стали сложнее.
seq
оценивает свой первый аргумент немедленно, а не только тогда, когда он нужен - это совсем не то же самое, что общая оценка функции.
Например
let x = 1+1
y = 2+2
in seq x (x, y)
сразу оценивает выражение 1+1
но нет 2+2
хотя ни то, ни другое не нужно оценивать немедленно. Образно, что возвращается
(2, 2+2)
не (1+1, 2+2)
,
Это иногда полезно, если вместо 1+1
у вас есть что-то вроде 1+2+3+...+1000000
это сравнительно дешевое вычисление, но его неоцененная, ленивая форма очень и очень длинна и занимает много памяти, что, если выражение не будет выполнено достаточно быстро, приведет к потере памяти; эта ситуация называется утечкой пространства в терминологии Хаскелла.
РЕДАКТИРОВАТЬ:
Чтобы ответить на ваш комментарий, вот фиктивный сценарий, в котором вложенная структура данных соответствует шаблону различной глубины. Структура данных перемежается с trace
звонки на каждом уровне, чтобы вы могли отслеживать, как это оценивается:
import Debug.Trace (trace)
data Age = Age Int
data Name = Name String
data Person = Person Name Age
name = trace "!NAME!" $ Name $ trace "!NAME CONTENT!" $ "John " ++ "Doe"
age = trace "!AGE!" $ Age $ trace "!AGE CONTENT!" $ 10 + 18
person = trace "!PERSON!" $ Person name age
-- person = trace "!PERSON!" $ Person (trace "!n!" name) (trace "!a!" age)
main = do
case person of p -> print "hello"
putStrLn "---"
case person of Person name age -> print "hello"
putStrLn "---"
case person of Person (Name str) age -> print "hello"
putStrLn "---"
case person of Person (Name str) (Age i) -> print "hello"
putStrLn "---"
case person of Person (Name str) (Age i) -> putStrLn $ "hello: " ++ str
putStrLn "---"
case person of Person (Name str) (Age i) -> putStrLn $ "hello: " ++ show (str, i)
Выход:
"hello"
---
!PERSON!
"hello"
---
!NAME!
"hello"
---
!AGE!
"hello"
---
hello: !NAME CONTENT!
John Doe
---
hello: ("John Doe",!AGE CONTENT!
28)
Обратите внимание, что выход из trace
звонки "мешает" выходу из putStrLn
/ print
вызовов, но на самом деле это довольно хорошо демонстрирует, как происходит оценка во время выполнения.
Кроме того, если вы определите Name
а также Age
с помощью newtype
вместо data
оценка будет немного отличаться как newtype
значения не имеют оболочки времени выполнения, поэтому представление памяти во время выполнения person
будет на один уровень "тоньше":
newtype Age = Age Int
newtype Name = Name String
data Person = Person Name Age
"hello"
---
!PERSON!
"hello"
---
"hello"
---
"hello"
---
hello: !NAME!
!NAME CONTENT!
John Doe
---
hello: ("John Doe",!AGE!
!AGE CONTENT!
28)