I've been learning about strict vs lazy data structures, and have been playing around with the :sprint
command in
ghci. My understanding of :sprint
is it displays the evaluation status of selected variables. I've come across the following curiosity which I can't understand.
ghci> data Foo = Foo{i::Int,j::String}
ghci> data Bar = Bar{i:: !Int, j::String}
ghci>
ghci>
ghci> a = Foo (3+2) "abc"
ghci> b = Bar (3+2) "abc"
ghci>
ghci> :sprint a
a = <Foo> _ _
ghci> :sprint b
b = _
My question is: why is a
evaluated to WHNF by default, but b
remains a thunk?
I was expecting the output of b
to be b = <Bar> 5 _
, which I can force by running seq b ()
.
ghci> seq a ()
()
ghci> seq b ()
()
ghci> :sprint a
a = <Foo> _ _
ghci> :sprint b
b = <Bar> 5 _
I believe it is because strict fields are just syntactic sugar, telling the compiler to automatically insert calls to seq
in certain places.
So the strictness annotation on Bar
means that b = Bar (3+2) "abc"
is actually compiled as something like b = let x = 3+2 in seq x (Bar x "abc")
.
After a = Foo (3+2) "abc"
, a
is a reference to an application of the constructor Foo
; its fields contain thunks. Constructors are treated specially, so GHCi's :sprint
can tell that the a
refers to a constructor application and shows it as a = <Foo> _ _
.
But after b = Bar (3+2) "abc"
, b
is a reference to an application of seq
, not directly to an application of the constructor Bar
. seq
is just a function; it's special in terms of its implementation, but not in terms of being represented specially in memory the way constructors are. A reference to a (non-constructor) function application is just a thunk, so GHCi shows it as it would any other thunk: b = _
.
Forcing the thunk referred to by b
would force the 3 + 2
and then result in a reference to an application of the Bar
constructor. But binding a variable doesn't automatically force the expression that is assigned to it.