간단한 용어로 설명하려고합니다. 다른 사람들이 지적했듯이, 머리 정상적인 형태는 Haskell에 적용되지 않으므로 여기서는 고려하지 않을 것입니다.
정상적인 형태
정상 형태의 표현은 완전히 평가되며, 하위 표현은 더 이상 평가 될 수 없습니다 (즉, 평가되지 않은 썽크를 포함하지 않음).
이러한 표현은 모두 정상적인 형태입니다.
42
(2, "hello")
\x -> (x + 1)
이 표현들은 정상적인 형태가 아닙니다 :
1 + 2 -- we could evaluate this to 3
(\x -> x + 1) 2 -- we could apply the function
"he" ++ "llo" -- we could apply the (++)
(1 + 1, 2 + 2) -- we could evaluate 1 + 1 and 2 + 2
약한 머리 보통 형태
약한 머리 정규 형식의 표현은 가장 바깥 쪽의 데이터 생성자 또는 람다 추상화 ( head ) 로 평가되었습니다 . 하위 표현 은 평가되거나 평가되지 않았을 수 있습니다 . 따라서 모든 일반적인 형식 표현은 머리의 일반 형식이 약하지만 반대는 일반적으로 적용되지 않습니다.
표현이 약한 머리 정규 형식인지 확인하려면 식의 가장 바깥 쪽 부분 만 살펴 봐야합니다. 데이터 생성자 또는 람다 인 경우 머리가 약한 정상적인 형태입니다. 함수 응용 프로그램 인 경우에는 그렇지 않습니다.
이 표현들은 약한 머리 정상적인 형태입니다 :
(1 + 1, 2 + 2) -- the outermost part is the data constructor (,)
\x -> 2 + 2 -- the outermost part is a lambda abstraction
'h' : ("e" ++ "llo") -- the outermost part is the data constructor (:)
언급 한 바와 같이, 위에 열거 된 모든 정규형 표현은 또한 약한 머리 정규형입니다.
이 표현들은 약한 머리 정상적인 형태가 아닙니다 :
1 + 2 -- the outermost part here is an application of (+)
(\x -> x + 1) 2 -- the outermost part is an application of (\x -> x + 1)
"he" ++ "llo" -- the outermost part is an application of (++)
스택 오버플로
약한 머리 정규형으로 표현을 평가하려면 먼저 다른 표현을 WHNF로 평가해야합니다. 예를 들어 1 + (2 + 3)
WHNF 로 평가 하려면 먼저 평가해야합니다 2 + 3
. 단일 표현식을 평가하면 이러한 중첩 평가가 너무 많아지면 스택 오버플로가 발생합니다.
이것은 많은 부분이 평가 될 때까지 데이터 생성 자나 람다를 생성하지 않는 큰 식을 만들 때 발생합니다. 이들은 종종 이런 종류의 사용으로 인해 발생합니다 foldl
.
foldl (+) 0 [1, 2, 3, 4, 5, 6]
= foldl (+) (0 + 1) [2, 3, 4, 5, 6]
= foldl (+) ((0 + 1) + 2) [3, 4, 5, 6]
= foldl (+) (((0 + 1) + 2) + 3) [4, 5, 6]
= foldl (+) ((((0 + 1) + 2) + 3) + 4) [5, 6]
= foldl (+) (((((0 + 1) + 2) + 3) + 4) + 5) [6]
= foldl (+) ((((((0 + 1) + 2) + 3) + 4) + 5) + 6) []
= (((((0 + 1) + 2) + 3) + 4) + 5) + 6
= ((((1 + 2) + 3) + 4) + 5) + 6
= (((3 + 3) + 4) + 5) + 6
= ((6 + 4) + 5) + 6
= (10 + 5) + 6
= 15 + 6
= 21
그것이 머리가 약한 정상 형태로 표현되기 전에 어떻게 깊이 들어가야하는지 주목하십시오.
왜 하스켈이 미리 내면의 표현을 줄이지 않습니까? 그것은 하스켈의 게으름 때문입니다. 일반적으로 모든 하위 표현식이 필요하다고 가정 할 수 없으므로 외부에서 표현식을 평가합니다.
(GHC에는 하위 분석기가 항상 필요한 일부 상황을 감지하여 미리 평가할 수있는 엄격한 분석기가 있습니다. 그러나 이는 최적화 일 뿐이므로 오버플로를 막기 위해 의존해서는 안됩니다.)
반면에 이런 종류의 표현은 완전히 안전합니다.
data List a = Cons a (List a) | Nil
foldr Cons Nil [1, 2, 3, 4, 5, 6]
= Cons 1 (foldr Cons Nil [2, 3, 4, 5, 6]) -- Cons is a constructor, stop.
모든 하위 표현식을 평가해야 할 때 이러한 큰 표현식을 작성하지 않으려면 내부 부분을 미리 평가해야합니다.
seq
seq
식을 강제로 평가하는 데 사용되는 특수 함수입니다. 그 의미는 약한 머리 정상 형태로 평가 seq x y
될 때마다 y
약한 머리 정상 형태로 평가됨을 의미합니다 x
.
의 정의에 사용 된 다른 위치는 foldl'
의 엄격한 변형입니다 foldl
.
foldl' f a [] = a
foldl' f a (x:xs) = let a' = f a x in a' `seq` foldl' f a' xs
반복 할 때마다 foldl'
누산기가 WHNF로 강제 설정됩니다. 따라서 큰 식을 작성하지 않으므로 스택 오버플로를 피할 수 있습니다.
foldl' (+) 0 [1, 2, 3, 4, 5, 6]
= foldl' (+) 1 [2, 3, 4, 5, 6]
= foldl' (+) 3 [3, 4, 5, 6]
= foldl' (+) 6 [4, 5, 6]
= foldl' (+) 10 [5, 6]
= foldl' (+) 15 [6]
= foldl' (+) 21 []
= 21 -- 21 is a data constructor, stop.
그러나 HaskellWiki의 예에서 언급했듯이, 어큐뮬레이터는 WHNF로만 평가되므로 모든 경우에 저장되지 않습니다. 단지 튜플 생성자의 평가, 강제하지 않도록 예에서, 누산기는 튜플 acc
또는 len
.
f (acc, len) x = (acc + x, len + 1)
foldl' f (0, 0) [1, 2, 3]
= foldl' f (0 + 1, 0 + 1) [2, 3]
= foldl' f ((0 + 1) + 2, (0 + 1) + 1) [3]
= foldl' f (((0 + 1) + 2) + 3, ((0 + 1) + 1) + 1) []
= (((0 + 1) + 2) + 3, ((0 + 1) + 1) + 1) -- tuple constructor, stop.
이를 피하려면 튜플 생성자를 평가할 때 acc
and의 평가를 강제로 수행해야합니다 len
. 우리는 이것을 사용하여 이것을한다 seq
.
f' (acc, len) x = let acc' = acc + x
len' = len + 1
in acc' `seq` len' `seq` (acc', len')
foldl' f' (0, 0) [1, 2, 3]
= foldl' f' (1, 1) [2, 3]
= foldl' f' (3, 2) [3]
= foldl' f' (6, 3) []
= (6, 3) -- tuple constructor, stop.