Чем отличается '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)
Другие вопросы по тегам