I\'m struggling to understand why these two snippets produce different results under the so-called \"poor man\'s strictness analysis\".
The first example uses data
The difference between data
and newtype
is that data
is "lifted" and newtype
isn't. That means the data
has an extra ⊥ -- in this case, it means that undefined
/= Parser undefined
. When your Applicative
code pattern-matches on Parser x
, it forces a ⊥
value if the constructor.
When you pattern-match on a data
constructor, it's evaluated and taken apart to make sure it's not ⊥. For example:
λ> data Foo = Foo Int deriving Show
λ> case undefined of Foo _ -> True
*** Exception: Prelude.undefined
So pattern matching on a data
constructor is strict, and will force it. A newtype
, on the other hand, is represented in exactly the same way as the type its constructor wraps. So matching on a newtype
constructor does absolutely nothing:
λ> newtype Foo = Foo Int deriving Show
λ> case undefined of Foo _ -> True
True
There are probably two ways to change your data
program such that it doesn't crash. One would be to use an irrefutable pattern match in your Applicative
instance, which will always "succeed" (but using the matched values anywhere later might fail). Every newtype
match behaves like an irrefutable pattern (since there's no constructor to match on, strictness-wise).
λ> data Foo = Foo Int deriving Show
λ> case undefined of ~(Foo _) -> True
True
The other would be to use Parser undefined
instead of undefined
:
λ> case Foo undefined of Foo _ -> True
True
This match will succeed, because there is a valid Foo
value that's being matched on. It happens to contained undefined
, but that's not relevant since we don't use it -- we only look at the topmost constructor.
In addition to all the links you gave, you might find this article relevant.