Как " 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

etc. В конечном счете, вещь, оцениваемая в любое время, является следующим примитивом 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 что является относительно дешевым вычислением, но его недооцененная, ленивая форма очень длинная и занимает много памяти, которая, если выражение не оценивается достаточно быстро, эффективно начнет утечку памяти; эта ситуация называется место утечки в терминологии Хаскелл.


EDIT:

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