16

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 (assuming a correct Applicative instance):

data Parser t a = Parser {
        getParser ::  [t] -> Maybe ([t], a) 
    }

> getParser (pure (,) <*> literal ';' <*> undefined ) "abc"
*** Exception: Prelude.undefined

The second uses newtype. There is no other difference:

newtype Parser t a = Parser {
        getParser ::  [t] -> Maybe ([t], a) 
    }

> getParser (pure (,) <*> literal ';' <*> undefined ) "abc"
Nothing

literal x is a parser that succeeds consuming one token of input if its argument matches the first token. So in this example, it fails since ; doesn't match a. However, the data example still sees that the next parser is undefined, while the newtype example doesn't.

I've read this, this, and this, but don't understand them well enough to get why the first example is undefined. It seems to me that in this example, newtype is more lazy than data, the opposite of what the answers said. (At least one other person has been confused by this too).

Why does switching from data to newtype change the definedness of this example?


Here's another thing I discovered: with this Applicative instance, the data parser above outputs undefined:

instance Applicative (Parser s) where
  Parser f <*> Parser x = Parser h
    where
      h xs = 
        f xs >>= \(ys, f') -> 
        x ys >>= \(zs, x') ->
        Just (zs, f' x')

  pure a = Parser (\xs -> Just (xs, a))

whereas with this instance, the data parser above does not output undefined (assuming a correct Monad instance for Parser s):

instance Applicative (Parser s) where
  f <*> x =
      f >>= \f' ->
      x >>= \x' ->
      pure (f' x')

  pure = pure a = Parser (\xs -> Just (xs, a))

Full code snippet:

import Control.Applicative
import Control.Monad (liftM)

data Parser t a = Parser {
        getParser ::  [t] -> Maybe ([t], a) 
    }


instance Functor (Parser s) where
  fmap = liftM

instance Applicative (Parser s) where
  Parser f <*> Parser x = Parser h
    where
      h xs = f xs >>= \(ys, f') -> 
        x ys >>= \(zs, x') ->
        Just (zs, f' x')

  pure = return


instance Monad (Parser s) where
  Parser m >>= f = Parser h
    where
      h xs =
          m xs >>= \(ys,y) ->
          getParser (f y) ys

  return a = Parser (\xs -> Just (xs, a))


literal :: Eq t => t -> Parser t t
literal x = Parser f
  where
    f (y:ys)
      | x == y = Just (ys, x)
      | otherwise = Nothing
    f [] = Nothing
Community
  • 1
  • 1
Matt Fenwick
  • 48,199
  • 22
  • 128
  • 192
  • 2
    When asking a question like this, it's generally better if you include all the relevant code, if it's small enough to fit (this includes the `Functor` and `Monad` instances, and `literal`), so that people don't have to guess at exactly how you wrote the functions (as you've pointed out, even small changes can make a difference in behavior). – shachaf Nov 26 '12 at 14:41
  • 1
    @shachaf the real question here is not "how do I fix my code?" -- I already did that -- but "what is different between `data` and `newtype` with respect to strictness/laziness?" Sorry if that's not clear from the question. – Matt Fenwick Nov 26 '12 at 14:44
  • Yes, but even so, how can we explain what was going on with your code without knowing what the code looks like? – shachaf Nov 26 '12 at 14:47
  • Presumably the behaviour with the Applicative instances comes from (in the first example) the fact that pattern matching is strict, i.e. both `Parser f` and `Parser x` are forced. (E.g `f (Just x) (Just y) = x; f (Just 1) undefined` throws an exception.) – huon Nov 26 '12 at 14:51
  • @shachaf hopefully it will help; I'm still hoping to understand this issue in a broader context than just how it applies to this specific problem though. – Matt Fenwick Nov 26 '12 at 15:14
  • [This article](http://blog.ezyang.com/2010/12/gin-and-monotonic/) explains the difference using Hasse diagrams, and shows the difference between lazy data, strict data, and newtype. – Matt Fenwick Nov 27 '12 at 16:26
  • I wrote a Haskellwiki article on [newtype](http://www.haskell.org/haskellwiki/Newtype) which explains some of the issues. – Ben Millwood Dec 06 '12 at 15:59
  • I had not run into this issue; but I think that I learned something useful from the explanations here. Thanks for posting! – Jesse Hallett Dec 12 '12 at 20:48

2 Answers2

22

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.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
hammar
  • 138,522
  • 17
  • 304
  • 385
  • Two things I'm still not quite clear on are 1) whether the pattern matching difference is necessitated by Haskell's semantics, and 2) whether the pattern matching difference is due to the constructor's strictness difference. – Matt Fenwick Nov 26 '12 at 16:25
  • 1
    @MattFenwick: 1) Yes, since newtypes basically don't exist semantically, pattern matching on one is the same as pattern matching on the underlying type, so since the pattern `x` doesn't evaluate `x`, neither does the pattern `N x`. 2) No. Consider `data S a = S !a; constS x (S y) = x`, then ```S undefined `seq` x = ⊥```, and `constS x ⊥ = ⊥`. – hammar Nov 26 '12 at 17:05
  • 1
    So in the case of a data constructor the compiler has to evaluate far enough to determine whether the constructor matches the pattern? – Jesse Hallett Dec 12 '12 at 20:47
  • @JesseHallett: Yes, even when there's only one constructor. – hammar Dec 13 '12 at 00:26
11

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.

shachaf
  • 8,890
  • 1
  • 33
  • 51