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
(<*)
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 whileconst (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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With