Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 814
Chris Stryczynski Avatar asked Jun 30 '19 17:06

Chris Stryczynski


1 Answers

(<*) 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
like image 135
duplode Avatar answered Nov 10 '22 09:11

duplode