Laziness/strictness between data and newtype

后端 未结 2 1203
无人共我
无人共我 2021-02-07 09:33

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

相关标签:
2条回答
  • 2021-02-07 10:05

    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.

    0 讨论(0)
  • 2021-02-07 10:20

    As you probably know, the main difference between data and newtype is that with data the data constructors are lazy while with newtype the data constructors are strict, i.e. given the following types

    data    D a = D a 
    newtype N a = N a
    

    then D ⊥ `seq` x = x, but N ⊥ `seq` x = ⊥.(where stands for "bottom", i.e. undefined value or error)

    What is perhaps less commonly known, however, is that when you pattern match on these data constructors, the roles are "reversed", i.e. with

    constD x (D y) = x
    constN x (N y) = x
    

    then constD x ⊥ = ⊥ (strict), but constN x ⊥ = x (lazy).

    This is what's happening in your example.

    Parser f <*> Parser x = Parser h where ...
    

    With data, the pattern match in the definition of <*> will diverge immediately if either of the arguments are , but with newtype the constructors are ignored and it is just as if you'd written

    f <*> x = h where
    

    which will only diverge for x = ⊥ if x is demanded.

    0 讨论(0)
提交回复
热议问题