3

In haskell IO type has instance of Monoid:

instance Monoid a => Monoid (IO a) where
    mempty = pure empty

if I have three actions which share some state, and change behavior of each other via side effect, this can lead to breaking associativity law, from IO type point of view:

a1:: IO String
a2:: IO String
a3:: IO String

(a1 mappend a2) mappend a3 /= a1 mappend (a2 mappend a3)

for example if a1,a2,a3 request current time in string, or IO contains some DB which counts for request number. This mean that it can be:

(a1 `mappend` a2) `mappend` a3  == "1"++"2"++"3"
a1 `mappend` (a2 `mappend` a3) == "3"++"1"++"2"

EDIT:

I think I shouldn't have given an example with a db, it confused, more preferred example:

a1 = show <$> getUnixTime 
a2 = show <$> getUnixTime
a3 = show <$> getUnixTime

l = (a1 `mappend` a2) `mappend` a3
r = a1 `mappend` (a2 `mappend` a3)
liftA2 (==) l r
**False**

So why IO type is monoid if it can break associativity law? or I missing something?

Will Ness
  • 70,110
  • 9
  • 98
  • 181
Evg
  • 2,978
  • 5
  • 43
  • 58
  • 2
    You think in terms of calling order, but due to laziness, that does not mean that `a1` will run last in the second case. – Willem Van Onsem Nov 21 '20 at 21:42
  • I think in terms of getting final result, due network latency or problems with DB it can be in any order. – Evg Nov 21 '20 at 21:47
  • 2
    Why not bring up the definition of `mappend` (as `(<>)`)? It's also vital to undertanding what's happening here. – Carl Nov 21 '20 at 21:48
  • 1
    @Evg: but `IO` (conceptually) does not *run* anything. An `IO` is a recipe to do something. You "sew" everything together into a huge recipe, and then this runs. – Willem Van Onsem Nov 21 '20 at 21:51
  • 5
    Also note that monoids aren't necessarily commutative. `m1 <> m2` is not necessarily the same as `m2 <> m1`, so the left-to-right order *matters*. Whether you think of this as combining `m1` and `m2` before combining the result with `m3`, or combining `m1` with the result of combining `m2` and `m3`, the end result is the same: an IO action that executes `m1`, then `m2`, then `m3`, in that order. – chepner Nov 21 '20 at 21:51
  • @WillemVanOnsem I edit the question, your comment with "IO recipe" helps a little bit, we comparing actions not results, yes synthetically the recipes are the same, but how can we consider these actions equal if they produce different result.. and If I try to test IO monoid associativity (liftA2 (==) l r) see in appended example with time it gives me False.. – Evg Nov 21 '20 at 22:36
  • @Evg: of course it does. The recipe does not say anything about the result, but the merging of recipies guarantees *order*, so your `a1`, `a2` and `a3` will, both in `l` and `r` be performed in the *same* order. – Willem Van Onsem Nov 21 '20 at 22:40
  • @Evg: as for your `liftA2 (==) l r`, this actually is extactly the same, you *first* will evaluate `l` and *then* you evaluate `r`. Notice that the *check* here is the victim. If you would use a `State` monad, you can the results more predictable and verify. – Willem Van Onsem Nov 21 '20 at 22:41
  • @Evg: in other words you make a recipe where you will first calculate `l`, then you calculate `r`, but meanwhile the "global system" state (likely) changes. – Willem Van Onsem Nov 21 '20 at 22:45
  • 1
    @Evg: in short, in both `l` and `r`, the actions `a1`, `a2` and `a3` will run in the same order. If these thus produce the same results, then the result will be the same. But something like `getUnixTime` depends on a *global* state; so then the result of `l` and `r` will simply be as unpredictable as `getUnixTime`. If you for example would "freeze" time, and after each time increment the clock, then it will return the same result for example. – Willem Van Onsem Nov 21 '20 at 22:48
  • @WillemVanOnsem It seems that with (liftA2 (==) l r) I try to test the internal value of IO for associativity :), but I must to test IO actions (not inner values), and they do not have EQ instance, so I can only to think of equality of them in context of freezed time as you said (or plus all state of the world). – Evg Nov 21 '20 at 23:05
  • Your `getUnixTime` example doesn't produce `False` because the order in which the `mappend` calls are evaluated, but because the uses of `getUnixTime` in `l` and `r` aren't executed simultaneously. None of the definitions of `a1`, `a2`, or `a3` actually *get* a time; they are just IO actions that, when executed, will get the time and pass the result to `show`. That won't actually happen until you try to execute the result of `liftA2 (==) l r` (which *also* only creates a new IO action without actually executing anything yet). – chepner Nov 21 '20 at 23:10
  • @chepner my misunderstanding was not in order, I expect associativity for wrong things) monoid fix associativity for actions, not for internal IO values. Actions a=a equal in any world. But to get values we need to provide world for actions and perform them, internal values of actions became same only if worlds are same. (a,w1) != (a,w2) but (a,w1) == (a,w1). – Evg Nov 21 '20 at 23:38
  • 1
    so no one answered the "why have it as a monoid then?" part. my guess is, so we can e.g. call `mconcat :: [IO a] -> IO a` to have a combined recipe that will `mconcat` the list of produced values. – Will Ness Nov 22 '20 at 13:49
  • The `Semigroup (IO a)` and `Monoid (IO a)` instances can be [derived via](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/deriving_via.html?highlight=derivingvia#extension-DerivingVia) [`Ap IO a`](https://hackage.haskell.org/package/base-4.14.0.0/docs/Data-Monoid.html#t:Ap): `deriving via Data.Monoid.Ap IO a instance Semigroup a => Semigroup (IO a)` ... `deriving via Ap IO a instance Monoid a => Monoid (IO a)`. – Iceland_jack Nov 22 '20 at 16:25

2 Answers2

14

a1 `mappend` (a2 `mappend` a3) does not run in the order a2, a3 and a1. In contrast to imperative languages like Python for example, in Haskell an IO a is not some result of a computation, it is a recipe to produce a value of a. You can actually see an IO more like a continuation in Python, you pass a function such that eventually it can be called, but you do not call it directly.

The mappend function is implemented as liftA2 (<>) for the Semigroup a => Semigroup (IO a) instance, as we can see in the source code:

instance Semigroup a => Semigroup (IO a) where
    (<>) = liftA2 (<>)

This thus means that mappend is implemented as:

mappendIO :: Semigroup a => IO a -> IO a -> IO a
mappendIO f g = do
    x <- f
    y <- g
    pure (x <> y)

so it runs f before g.

If we now look at (a1 `mappend` a2) `mappend` a3, we see:

(a1 `mappend` a2) `mappend` a3 = do
    x <- do
        x1 <- a1
        x2 <- a2
        pure (x1 <> x2)
    y <- a3
    pure (x <> y)

which is equivalent to:

(a1 `mappend` a2) `mappend` a3 = do
    x1 <- a1
    x2 <- a2
    x3 <- a3
    pure ((x1 <> x2) <> x3)

If we then look at a1 `mappend` (a2 `mappend` a3) then this is equivalen to:

a1 `mappend` (a2 `mappend` a3) = do
    x <- a1
    y <- do
        y1 <- a2
        y2 <- a2
        pure (y1 <> y2)
    pure (x <> y)

which is equivalent to:

a1 `mappend` (a2 `mappend` a3) = do
    x1 <- a1
    x2 <- a2
    x3 <- a2
    pure (x1 <> (x2 <> x3))

Since x1 <> (x2 <> x3) is equivalent to (x1 <> x2) <> x3, this will thus return the same result in both items.

As for your test:

l = (a1 `mappend` a2) `mappend` a3
r = a1 `mappend` (a2 `mappend` a3)
liftA2 (==) l r
False

Notice that the liftA2 (==) again will define a sequence, so that means that your liftA2 (==) l r is defined as:

liftA2 (==) l r = do
    x1 <- a1
    x2 <- a2
    x3 <- a3
    y1 <- a1
    y2 <- a2
    y3 <- a3
    pure ((x1 <> x2) <> x3) == (y1 <> (y2 <> y3))

You thus run r after l.

If you make use of State monad, you can make it more clear what will happen, and validate if the rule is applied. You need to reset the state between l and r however.

Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
  • 1
    I wouldn't necessarily bring up laziness; I don't think it's terribly relevant. And I certainly wouldn't lead with it! – dfeuer Nov 23 '20 at 01:01
7

You cannot use liftA2 (==) to meaningfully compare IO values: this comparison relation is not even reflexive!

Indeed, if we run

a1 = show <$> getUnixTime 
liftA2 (==) a1 a1

It is possible to get a False result, since time passes between the two calls to getUnixTime, so the returned value might differ.

This is even more clear if you define a1 to return the value of some random number generator. Calling that twice would yield a different result almost always.

Yet another example: liftA2 (==) getLine getLine can return false if the user enters two different lines.

When we say that ioAction1 is equal to ioAction2 we mean that they would have the same effect if executed in the exact same context. This is not the same as executing one action after the other and comparing the results.

Precisely defining "same IO effect" is tricky, though, since we usually want yo ignore performance differences. E.g. return () >> print True might be slightly slower than print True, if no optimization is performed, still we want to regard these two actions as having the same effects.

chi
  • 111,837
  • 3
  • 133
  • 218