5
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

Chris Stryczynski
  • 30,145
  • 48
  • 175
  • 286
  • 4
    `(<*)` doesn't use the `b` values from the `f b`, but it does use the `f` effects (for instance, using `\x -> return x <* print x` will actually print `x`), so it must inspect the second argument. – duplode Jun 30 '19 at 17:33
  • How/why is it used though? What is responsible for this? – Chris Stryczynski Jun 30 '19 at 17:37
  • 1
    @ChrisStryczynski The runtime system executes `main`, and is responsible for discovering all the `IO` effects reachable from it. – Daniel Wagner Jun 30 '19 at 17:38
  • 7
    This doesn't happen just with `IO`, either. For instance, while `Just 1 <* Just 2` is `Just 1`, `Just 1 <* Nothing` is `Nothing`. There is no way to figure that out without inspecting the second argument. – duplode Jun 30 '19 at 17:42
  • 4
    Imagine if one wrote `putStrLn "Hello!" *> putStrLn "World!"`. What would the output be? – AJF Jun 30 '19 at 17:43
  • Oh that is a good example. Why does that execute both while `const (print "test") (print "test2")` does not? – Chris Stryczynski Jun 30 '19 at 17:45
  • Okay so that narrows it down even further to: `fmap (const) (putStrLn "test")` printing `test`... Which I suppose comes down to the implementation of the IO Monad? – Chris Stryczynski Jun 30 '19 at 17:50
  • @ChrisStryczynski "Which I suppose comes down to the implementation of the IO Monad?" -- Yup, pretty much so. – duplode Jun 30 '19 at 17:52
  • 1
    consider `const <$> Just 1 <*> Just undefined`, `const <$> Just 1 <*> undefined`, `runIdentity $ const <$> Identity 1 <*> undefined` (you might need to `import Data.Functor.Identity` for the last one). So yes, it's in the definition of the applicative instance for the particular functor. (see also: https://stackoverflow.com/questions/24467803/how-to-handle-side-effect-with-applicative/24467994#24467994) – Will Ness Jun 30 '19 at 18:00
  • 1
    `xyz = pure undefined` will do what you expect—`<*` is (for most applicatives) strict in the effects, but it’s non-strict in the right operand’s *result* – Jon Purdy Jun 30 '19 at 18:53

1 Answers1

4

(<*) 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
duplode
  • 33,731
  • 7
  • 79
  • 150