1

Consider

ioFunction :: String -> IO ()
ioFunction str = do putStrLn str
                    putStrLn "2"

Here, ioFunction, from a mathematical point of view, is a function that takes one input and returns a value of type IO (). I presume it means nothing else. That is, from a mathematical point of view, this function returns a value and nothing else; in particular, it doesn't print anything.

So does this mean that the way Haskell uses the IO monad for imperative side-effects (in this case: that running this function will first print str and then print "2", in that order) is purely a GHC implementation detail that has nothing to do with the mathematical (and in some sense even the Haskell) meaning of the term?

EDIT: To make this question clearer, what I mean to ask is, for example, is there any difference from the mathematical point of view between the following two functions:

ioFunction1 :: String -> IO ()
ioFunction1 str = do putStrLn str
                     putStrLn "2"


ioFunction2 :: String -> IO ()
ioFunction2 str = do return ()

It seems the answer is no, for -- from the mathematical point of view -- they both take as input a String and return a value (presumably, the same value) of type IO (). Is this not the case?

George
  • 6,927
  • 4
  • 34
  • 67
  • It's a bit confusing what you mean. Monads can be used for instance to carry a *hidden* state so to speak and indeed enforce order in execution. The IO monad has in my opinion much in common with the state monad except that it hides all kinds of undeterministic behavior an operating system inherently has. – Willem Van Onsem Jan 29 '17 at 22:58
  • What I'm saying is: from a GHC point of view, ioFunction first prints `str` and then prints "2". Yet from a mathematical point of view, this function only take a `String` and returns `() :: IO ()`. Hence the fact that we happen to use the IO monad for side effects in Haskell is in some sense arbitrary, and has nothing to do with the intrinsic meaning of a (mathematical) monad (of type IO). – George Jan 29 '17 at 23:00
  • 1
    No the function does not return `()`, it returns an IO monad (that is here a function) and that monad will eventually return `()`. That is something different. And in fact the IO has some *intrensic* mathematical meaning. – Willem Van Onsem Jan 29 '17 at 23:02
  • `() :: IO ()` is a type error. `()` has type `()`. – melpomene Jan 29 '17 at 23:03
  • Yes -- you're right. It returns a value of type `IO ()`. So how is that value a function though? And from a purely mathematical point of view, what is this value? – George Jan 29 '17 at 23:04
  • @WillemVanOnsem The function does not return "an IO monad". There's only one IO monad, and it's not a value. It's a type constructor (of kind `* -> *`) together with an `instance` declaration. – melpomene Jan 29 '17 at 23:05
  • @melpomene: yeah indeed. I chose my words poorly. – Willem Van Onsem Jan 29 '17 at 23:06
  • 1
    @George I don't think it is a function. – melpomene Jan 29 '17 at 23:06
  • @melpomene: From a mathematical perspective, does the value that is returned by this function encode anything that has anything to do with those `putStrLn` calls? I know that, from GHC's perspective, those `putStrLn` calls are definitely part of this function's meaning. But from the mathematical perspective, they could just as easily be ignored, isn't this the case? – George Jan 29 '17 at 23:16
  • Define "mathematical perspective". There is more than one. The value returned by `putStrLn "2"` has an action that prints `2` encoded in it (otherwise your program wouldn't be able to print anything). It's a fact. You can build a mathematical apparatus to describe and study this and similar facts. It probably would be different from the one that deals with functions and pure computations. – n. m. could be an AI Jan 30 '17 at 16:33
  • All this discussion misses the point of the mathematical definition of "monad", which is used to encapsulate side effects. See my answer how this definition can be brought in. – tillmo Feb 16 '17 at 12:59
  • Possibly related: https://stackoverflow.com/questions/61798648/how-to-view-higher-order-functions-and-io-actions-from-a-mathematical-perspectiv . – atravers Oct 17 '20 at 03:20

3 Answers3

5

Here, ioFunction, from a mathematical point of view, is a function that takes one input and returns a value of type IO (). I presume it means nothing else.

Yes. Exactly.

So does this mean that the way Haskell uses the IO monad for imperative side-effects (in this case: that running this function will first print str and then print "2", in that order) is purely a GHC implementation detail that has nothing to do with the mathematical (and in some sense even the Haskell) meaning of the term?

Not quite. From a mathematical (set-theory) standpoint, I would assume "same" means structurally identical. Since I don't know what makes up a value of IO (), I can't say anything about whether two values of that type are the same or not.

In fact, this is by design: by making IO opaque (in the sense that I don't know what constitutes an IO), GHC prevents me from ever being able to say that one value of type IO () is equal to another. The only things I can do with IO is exposed through functions like fmap, (<*>), (>>=), mplus, etc.

Alec
  • 31,829
  • 7
  • 67
  • 114
  • 2
    @George Yep. I am of the opinion this should be one of the first things discussed about `IO` - the fact you _can't_ and _shouldn't_ be able to say anything about what structurally makes up an `IO` action. – Alec Jan 29 '17 at 23:52
  • 2
    @George I'd say that, since IO is opaque, one could have an IO "model" (i.e. a semantics) where all IO actions (of the same type) are equal, e.g `print 5 === return ()`. Essentially, we take `IO a = Const () a`. This would be a trivial IO model which should satisfy all the wanted laws (I guess). This is what you seem to be mentioning in the question. However, IO does not have to be implemented in this (useless) way, and more meaningful models/semantics exist. When dealing with all the possible models, we can not regard `print 5` to be equivalent to `return ()`. – chi Jan 30 '17 at 00:36
4

I always find it helpful to consider a simplified “imperative-language source code” implementation of the IO monad:

data IO :: * -> * where
   PutStr :: String -> IO ()
   GetLine :: IO String
   IOPure :: a -> IO a
   IOSeq :: IO a -> IO b -> IO b
   ...
   LaunchMissiles :: IO ()

Then what ioFunction is quite clearly a proper, sensible function in the mathematical sense:

ioFunction str = do putStrLn str
                    putStrLn "2"
               = putStr (str++"\n") >> putStrLn "2\n"
               = IOSeq (PutStr (str++"\n")) (PutStrLn "2\n")

This is simply a data structure representing effectively some source code of an imperative language. ioFunction places the given argument in a specific spot in the result structure, so it's mathematically very much not just a trivial “return () and nothing else (something may happen but it's a GHC implementation detail)”.

The value is indeed completely different for ioFunction2:

ioFunction2 str = do return ()
                = return ()
                = IOPure ()

how do we know they are different in this sense?

That's a good question. Practically speaking, you know it by executing both programs and observing that they cause different effects, hence they must have been different. This is more than a little awkward of course – “observing what happens” isn't maths, it's physics, and the observation would scientifically require that you execute twice under exact the same environment conditions.

Worse, even with pure values that are generally taken to be mathematically the same you can observe different behaviour: print 100000000 will immediately cause the side-effect, whereas print $ last [0..100000000] takes a significant pause (which, if you follow it with a time-printing command, can actually yield different text output).

But these issues are unavoidable in a real-world context. It hence makes only sense that Haskell's does not specify any structure on IO that could with mathematical rigour be checked for equality from within the language. So, in quite some sense you really can't know that putStrLn str >> putStrLn "2" is not the same as return ().

And indeed they might be the same. It is conceivable to make a even simpler toy implementation of IO than the above thus:

data IO :: * -> * where
   IONop :: IO ()
   IOHang :: IO a -> IO a

and simply map any pure output operation to the no-op (and any input to an infinite loop). Then you would have

ioFunction str = do putStrLn str
                    putStrLn "2"
               = IONop >> IONop
               = IONop

and

ioFunction2 str = return ()
                = IONop

This is a bit as if we've imposed an additional axiom on the natural numbers, namely 1 = 0. From that you can then conclude that every number is zero.

Clearly though, that would be a totally useless implementation. But something similar could actually make sense if you're running Haskell code in a sandbox environment and want to be utterly sure nothing bad happens. http://tryhaskell.org/ does something like this.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • Well said. But isn't it true that, from a mathematical point of view, both `IOSeq (PutStr (str++"\n")) (PutStrLn "2\n")` and `IOPure ()` are in fact the same value? Or at the very least, how do we know they are different in this sense? – George Jan 29 '17 at 23:23
  • 1
    No, they are not mathematically speaking the same value. _How_ we know they are different is indeed sort of an implementation detail – in such an explicit “imperative code” IO monad, we could simply pattern-match to tell the difference; in compiled GHC this is a bit trickier. You might even devise a “dummy output” implementation where they are in fact the same. At any rate, without knowledge of the implementation you can't _conclude_ they are the same. – leftaroundabout Jan 29 '17 at 23:31
0

For simplicity, let us concentrate on the output aspect of the I/O monad. Mathematically speaking, the output (writer) monad is given by an endofunctor T with T(A) = U* × A, where U is a fixed set of characters, and U* is the set of strings over U. Then ioFunction : U* → T (), i.e. ioFunction : U* → U* × (), and ioFunction(s) = (s++"\n"++"2\n", ()). By contrast, ioFunction2(s) = ("", ()). Any implementation has to respect this difference. The difference is primarily a mathematical one, not an implementation detail.

Quonux
  • 2,975
  • 1
  • 24
  • 32
tillmo
  • 607
  • 5
  • 11