Chris Stryczynski
Chris Stryczynski

Reputation: 33911

Why isn't this this applicative statement being lazily evaluated, and how can I understand why?

abc :: IO (Int)
abc = do
  print "abc"
  pure $ 10

xyz :: IO (Int)
xyz = undefined

main :: IO () 
main = do
  x <- (((+) <$> abc <*> abc) <* xyz)
  print x

Why in the above is xyz being evaluated? I would assume due to Haskell's lazy nature it would not need to evaluate xyz (and hence not reach the undefined)?

My assumption is based on the type of <*:

Prelude> :t (<*)
(<*) :: Applicative f => f a -> f b -> f a

Following on with:

    -- | Sequence actions, discarding the value of the first argument.
    (*>) :: f a -> f b -> f b
    a1 *> a2 = (id <$ a1) <*> a2

And:

(<$)        :: a -> f b -> f a
(<$)        =  fmap . const

And hence f b never gets used.


Is there a way I can understand / investigate why this is being evaluated strictly? Would looking at the GHC compiled Core be helpful in this?


Thanks to the discussion in the comments it seems (please someone correct me if I'm wrong) it's due to the Monad implementation of the IO because the following two statements seem to evaluate differently:

Identity:

runIdentity $ const <$> (pure 1 :: Identity Int) <*> undefined
1

IO:

const <$> (pure 1 :: IO Int) <*> undefined
*** Exception: Prelude.undefined

Upvotes: 5

Views: 122

Answers (1)

duplode
duplode

Reputation: 34378

(<*) doesn't use the b values from the f b, but it does use the f effects, so it must inspect the second argument.

Why does [putStrLn "Hello!" *> putStrLn "World!"] execute both while const (print "test") (print "test2") does not?

In the type of const...

const :: a -> b -> a

... both a and b are fully parametric, and there is nothing else to deal with. With (<*), though, the situation is rather different. For starters, (<*) is a method of Applicative, so anyone writing an Applicative instance for IO can supply a concrete...

(<*) :: IO a -> IO b -> IO a

... implementation that uses IO-specific functions to combine effects from the two arguments in whatever way is deemed necessary.

Furthermore, even if (<*) weren't a method of Applicative, its type...

(<*) :: Applicative f => f a -> f b -> f a

... is such that, though a and b are fully parametric, f is not, because of the Applicative constraint. Its implementation can use other methods of Applicative, which can, and in most cases will, use the effects from both arguments.

Note that this is not an IO-specific issue. For instance, here is (<*) @Maybe not ignoring the effects of its second argument:

GHCi> Just 1 <* Just 2
Just 1
GHCi> Just 1 <* Nothing
Nothing

Upvotes: 4

Related Questions