Monadic IO: Laziness Makes You Free
underscore.io
underscore.io
So is this whole IO Monad idea is to simply delay the side effect to the edges? e.g. no pure program is useful, as it will have no input and no output, but if you separate the input and output from computation and delay side effects to the very end using monadic transformations you end up with more "pure" functions and less "side effects" functions? (you can never have zero side effects if you want your code to actually do anything)
This is how I ELI5 this to myself, but not sure if this is the right way to put it. Am I close?
I see it as this - it's simply one of the SOLID principles. (the first one)- Separation of Concerns
The best example (that doesn't involve Monads) that I can think of is this:
def logMessage(foo:String, bar:String) = println(s"your $foo has gone $bar")
def logMessage(foo:String, bar:String) = s"your $foo has gone $bar"
This is obviously not a monad or has anything to do with functional programming, but the second one has no side effects, yet is still useful.In the end the CLIENT of the code can decide what side effects they can do with it (println, log.debug, whatever)
tl;dr - I don't really get all this Monadic stuff, but I see this as simple SOC principle, decouple the data from it's presentation, or in other words, the library should work with pure data, the client should work with the "unpure" input and "unpure output", the library should just transform the input DATA into pure output DATA. the side effects are delayed to the CLIENT instead of the LIBRARY code.
How far am I? Hope it doesn't sound too simplistic (or just plain dumb)
If you look at another comment I left on this thread you'll see one implementation of this. While it's very complex, it does have a very nice interpretation of this system as the "RTS" and the "pure data structure" working as co-routines, passing control back and forth between one another.
The "monadic" part is pretty much a red herring. It's just how Haskell happens to implement this separation. It could also be implemented as an "effect system" built into the language, or any number of other ways.
Separating I/O concerns from the rest of your program can obviously be done in a number of ways. In Java or C++, I'd expect it to be done by doing all I/O in one class - either in main() or, in a bigger program, in a *OutputAdapter. If you keep all your impurity in the main loop and call exclusively pure functions from there, then yes, you end up with something that looks like the simplest case of Haskell's IO. But that programming style doesn't translate well to a functional model. It also doesn't provide, as Haskell does, a language-level guarantee that those functions you're calling actually are pure.
A monad doesn't have to represent impurity, and having impurity doesn't mean that you have to have a monad. You could just write your functions to carry around tuples where the first element is the actual value and the second is the context. But that doesn't scale when you want to be able to handle an arbitrary number of types of state in the same way, and it just so happens that the ways in which you generally want to compose stateful operations correspond to the mathematical construct of a monad.
If you say, for example:
x = print "Foo"
then the value of x is a program that prints "Foo" when executed and in y = x >> x
the value of y is a program that prints "Foo" twice.The language runtime essentially executes the value of "main" (which may be lazily evaluated).
Where monads come into play is the composition of such programs, especially when a program may produce output that needs to be fed to another program. The "bind" function does that, and the Monad typeclass abstracts the pattern so that you're not limited to just sequential programs; for example, promises (asynchronously executed programs) are monadic as well.
So to say it again in slightly different words: when you are writing Haskell, your job is to generate `main :: IO ()` (if you have never read Haskell type annotations, that's short for "a thing named main which is a program which produces nothing important." To do so, you have a lot of building blocks, like `getLine :: IO String` (a thing named getLine which is a program which produces a String), `putStrLn :: String -> IO ()` (a function named putStrLn which takes a String as input and produces a program which produces nothing important).
You also have functions which combine these together, like `(>>=) :: IO x -> (x -> IO y) -> IO y` (an operator named >>= which takes, on the left, a program which produces an x, and, on the right, a function which turns an x into a program which produces a y -- the operator then giving you a program which produces a y.)
So one example is:
main :: IO ()
main = getLine >>= hello where
hello :: String -> IO ()
hello name = putStrLn ("Hello, " ++ name ++ "!")
This is a program which prompts the user for a line (like, say, `Chris`), wraps it in some text, and prints that text out.The Haskell compiler just writes `main` to disk as the executable. The compiler does not "run" any of these programs; that happens when you run the executable which Haskell makes for you.
It is called by this scary word "monad" because that describes two things you can do with programs, one of which is this fundamental `>>=` operator.
It turns out this style of recipe-building, when you get down to the minimal stuff you need for it, is really useful for any sort of computation-in-a-context (for a recipe, the result of running the recipe is the value "in" the context - you can "modify" it by adding to the recipe, same as you can "modify" a function's return value by composing it with another function). Basically, the interface says that "return" lets you put something in a context, "join" describes how you can compose contexts, and kleisli-compose says that if you have two functions that return results in a context ( a -> ctx(b) and b -> ctx(c) ), you can compose those functions by composing the resulting contexts with "join". The end result is that if you have some context, like a recipe, you can apply changes to it ("bind"). In fact, if you have no context (the Identity monad) kleisli-compose reduces to just normal function composition, and bind reduces to application.
TL;DR: separation of concerns is a really nice side-effect of what's just a really broadly applicable interface for "more complicated but still composable stuff".
edit: I was somewhat inaccurate here - as the name implies, `Pure.println` is pure, but it returns a thunk which is not. That's what I'm talking about, I have trouble understanding how this isn't just a little dance you do to pretend your code isn't impure.
As the author of the article I'm probably not the best person to give a different perspective on the article. Hope that helps in any case.
This nets us tests that are 100% reliable. The compiler rejects all code that can introduce external inputs.
Based on the answers here and prior thought, I believe that Monads are Noble Lies [1]. Impurity is a hard concept. Purity is the goal. So rather than accepting into a well designed model the idea of impurity one should say something is pure even if it actually thunks at the end. This keeps the overall model sound. For people that really don't want or need or can't get into the details of real life mutability, you can keep the model as is with the lie of glossing over the thunk.
This is what we're doing here. We very explicitly separate our program into two times -- giving the instructions, and shouting "go!" (calling run). Before we shout "go!", substitution holds. Once we shout "go!" we don't give instructions any more.
In Haskell you don't even call `run` -- the runtime does this for you.
There is no Noble Lie. There is a time (a world if you like) where substitution holds. We assemble our program in this time / world. There is a time / world where the program is run. We don't construct the program in this world. (This may sound restrictive, but it isn't.)
Play with the code. The example is straightforward and easy to extend.
-- set of commands
data Teletype = Write String x | Read (String -> x) deriving Functor
-- monadic data structure
data TeletypeM a = Pure a | Effect (Teletype (TeletypeM a))
-- monad instance
instance Monad TeletypeM where
return = Pure
m >>= k = case m of
Pure a -> k a
Effect tt -> Effect (fmap (>>= k) tt)
-- API
write :: String -> TeletypeM ()
write s = Effect (Write s ())
read :: TeletypeM String
read = Effect (Read (\s -> s))
Now using this we can write "effectful" teletype programs. For instance, the "echo doubler" echoTwice :: TeletypeM ()
echoTwice = forever $ do
line <- read
write line
write line
Finally, we can "interpret this into IO", which is to say give it a side-effecting semantics runTeletypeMIO :: TeletypeM a -> IO a
runTeletypeMIO m = case m of
Pure a -> return a
Effect e -> case e of
Write s next -> do
putStrLn s
runTeletypeMIO next
Read cont -> do
line <- readLine
let next = cont line
runTeletypeMIO next
This is nice and feels a lot like "pushing the toothpaste around", but we can take advantage of its pure structure for other purposes. For instance, here's a delimited interpreter which only allows it to take a certain number of "steps" runTTDelimited :: Int -> TeletypeM () -> IO ()
runTTDelimited n m
| n <= 0 = return ()
| otherwise = case m of
Pure a -> return a
Effect e -> case e of
Write s next -> do
putStrLn s
runTTDelimited (n-1) next
Read cont -> do
line <- readLine
let next = cont line
runTTDelimited (n-1) next
And here's a completely pure interpreter version of the above which also takes its input from a pre-arranged "script" runTTPure :: Int -> TeletypeM () -> ([String] -> [String])
runTTPure n m input = reverse (go n m input []) where
go n m input acc
| n <= 0 = acc
| otherwise = case m of
Pure a -> acc
Effect e -> case e of
Write s next -> go (n-1) next input (s : acc)
Read cont -> case input of
[] -> error "ran out of input"
(s : ss) -> go (n-1) (cont s) ss acc
All of these can work off of the same TeletypeM values.An important note to consider is that Haskell has a defined order of computation just like most other languages.
The thunk will be evaluated based on certain rules, combined with how Monads are chained (using `>>=` or bind) this causes the effects to naturally occur in the correct order when the thunks are evaluated.
Put another way the thunks being impure is safe because the order of evaluation of the thunks is well defined.
Note that evaluation order of pure operations is well defined but hard to pin down, as "outside in" is hard to grasp. However since those methods are pure the order isn't important.
Of course, I assume you can rebuild the concept of execution context, replacing the stack with some other context abstraction. You've just recreated yet another powerful and standard concept -- in addition to exceptions and control flow -- provided by the platform. I guess that when using Haskell the entire platform and all libraries already use this monadic context (as there's no concept of a stack), but on the JVM you're now living in a "shadow" world, completely separate from any built-in abstraction.
Laziness (on imperative platforms) may have its virtues, but its cost is that it makes you abandon and then re-create in a foreign and incompatible way lots of powerful -- and standard -- execution primitives.
We can easily remove booleans from a language by combining introduction and elimination into one step. In other words, we can replace introduction/elimination combos like this:
bool b = lessThan(x, y);
if (b) { foo; } else { bar; }
With a single construct, like this: ifLessThan(x, y) { foo; } else { bar; }
We can do this to every boolean introduction rule; in particular, if we do it to the constants 'true' and 'false' we end up with a form of the "Church booleans"[2]: ifTrue { i_will_always_run; } else { i_will_never_run; }
ifFalse { i_will_never_run; } else { i_will_always_run; }
If we think about first-order programs, what effect does this transformation have? It brings together the reason for a decision with the consequences of that decision. In other words, to use 'ifLessThan' we must have the numbers ('x' 'y') which 'do the choosing' and the branches ('foo' 'bar') which get chosen, at the same time.What if we inspect the stacks of these programs? They will always show us the reason for branches being taken! We're never just in the branch of an 'if' construct: we're in the branch of an 'ifLessThan' construct, and we can see what the numbers were; or an 'ifEqual' branch; or an 'ifPhaseOfTheMoon' branch; or whatever. In other words, to paraphrase your complaint about monadic IO, booleans erase all context!
In a language with booleans, we can write a complex, deep calculation which returns a boolean; then we can use that boolean elsewhere. Reasoning about such programs is incredibly difficult, since all of the stack information built up during our complex calculation is thrown away once it returns; by the time we hit a problem using the boolean, we know nothing about it other than 'true'/'false' (this is also related to the idea of "boolean blindness"[3]).
This same argument can actually be applied to every data type (numbers, lists, etc.).
So, why do languages use booleans? Because despite these problems, this ability to split up reasons from consequences can be very useful too! It lets reify decisions into concrete values. We can manipulate these values with an intuitive, high-level algebra (AND/OR/NOT/etc.). It lets us separate how we make decisions from what those decisions are. As long as these decisions are given descriptive variable names like "queue_is_empty", and we avoid the temptation to collapse everything together with logical connectives, then we can still reason effectively: "we got here because the queue was empty".
In a language without booleans, we can regain this power using higher-order functions (passing branches around as variables); or we can write first-order boilerplate to model the same thing. But why bother when we can just use booleans?
Exactly the same argument can be applied to reified ('monadic') IO. The core idea is to represent IO actions as data, just like booleans represent decisions. Just like with booleans, this lets us separate how we decide on actions from what those actions are. Likewise, we can use (more or less) intuitive, high-level algebras to manipulate these values. Monads are one example of an algebra for doing this; applicatives, effects and arrows are other algebras we could use instead. Note that we can also manipulate boolean values using algebras other than "boolean algebra"; eg. as a ring.
Yes, we lose the context of what caused an action to be decided on; but it's exactly the same as losing the context of what caused a boolean to be true/false. The "solution" is the same in both cases too: look at how it was produced, not how it's used.
Booleans are either "true" or "false", not matter which operations went into their construction. We can't 'inspect' a boolean to recover whether it 'contains' 'AND's/'OR's/'other booleans', etc. Likewise, trying to 'inspect' an IO action to recover whether it 'contains' 'PrintLn's/'open's/etc. is just as futile and defeats the point of IO actions.
In other words, if you're recreating/rebuilding things to work with IO actions then you're DoingItWrong(TM). Trying to, for example, look for a "println" call 'inside' an IO action is like trying to look for an "OR(x < 5, ...)" call 'inside' a boolean.
[1] http://en.wikipedia.org/wiki/Natural_deduction#Introduction_... [2] http://en.wikipedia.org/wiki/Church_encoding#Church_Booleans [3] https://existentialtype.wordpress.com/2011/03/15/boolean-bli...
> Trying to, for example, look for a "println" call 'inside' an IO action is like trying to look for an "OR(x < 5, ...)" call 'inside' a boolean.
Perhaps, but they're both extremely useful, and luckily, if you keep to the very powerful imperative abstractions provided by the JVM, you can do both:
> Reasoning about such programs is incredibly difficult, since all of the stack information built up during our complex calculation is thrown away once it returns
On the JVM, it's quite easy to transform any computation to record all state-changes and decisions (AKA "omniscient debugging")[1]. Of course, using monads will make things that much harder, because now you have to record not only a very elaborate object graph, but one that is detached from a thread. Production-time omniscient debugging tools can't do that.
> this lets us separate how we decide on actions from what those actions are.
Oh, absolutely; it's a terrific abstraction. Now all that's left to do is weigh the cost of the benefits of this abstraction against its cost.
The costs include erasing context and making post-hoc reasoning hard (both in debugging and profiling); also, this abstraction is infectious -- so it's hard to limit it to just the places where you need it -- and incompatible with code that doesn't use it. The advantage is manipulation IO operations much more easily that with bytecode transformers (as the JVM already treats all code as data, and makes it available for inspection and manipulation).
In short, this beautiful abstraction lets you do something that can already be done on all code on the JVM, but more easily, except it's limited to code that actually uses it, which makes it incompatible with code that doesn't (i.e. almost all code).
Why would I pay so dearly to do something I can already do much more generally?
[1]: Open source: http://www.lambdacs.com/debugger/, http://pleiad.dcc.uchile.cl/tod/download.html commercial: http://chrononsystems.com/, production time: https://www.takipi.com/
For the same reason you might pay to use a typed language, when an untyped language is more general. Or why you might pay for encapsulation, when globals are more general. Or why you might pay for structured programming, when GOTOs are more general. Or why you might pay for a VM, when machine code is more general. And so on.
I also don't see why it's difficult to mix and match monadic/non-monadic IO. We can convert back and forth easily: "return" turns a non-IO value into an IO action and "run" (AKA "unsafePerformIO") turns an IO action into a regular value. We can think of it as just delaying and forcing function calls; in the same way that we can think of booleans as their Church encodings.
What's more, in an imperative context like the JVM, everything is already in IO by default, so we never need to do any conversion! Nothing becomes 'incompatible', since the JVM lets us perform side-effects whenever we like.
I think the more important distinction between the JVM and IO a-la Haskell is the laziness in Haskell. Without laziness, it would be awkward for a JVM language to abstract this stuff; in the same way that it's awkward to write ifThenElse as a function, or to write short-circuiting boolean operators.
Not all abstractions are created equal, and while all the other ones you mention do have some costs, they are dwarfed by the benefits; they're bargains. This one? I'd say it's overpriced junk (though it's pretty junk). The expressiveness it buys on top of what's already there (threads) is negligible, and it's very pricey.
Not only do abstractions differ in benefits and costs in general, those costs differ considerably depending on the platform.
> We can think of it as just delaying and forcing function calls
Delaying a "plain IO" operation is no different (in fact, it's identical) to just using a thread. The abstraction is already built in. The laziness of Haskell is a dual to (blocking) threads, and threads are already well supported. There's no need to replace them, especially if doing so severely harms your ability for posthoc reasoning (I'd say that even Haskell sacrifices posthoc reasoning for apriori reasoning, which is a wrong choice for most uses; but that's doubly worse on the JVM, which already has a lot of powerful tooling in place for working imperatively).
Edward Kmett explores a different method based on a kind of optimization of free monads and Richard Kieburtz's `OI` monad [0]. In particular, we consider values of the type
data OI a where
OI :: i -> (o -> a) -> FFI i o -> OI a
To decode this a bit, consider the abstract type `FFI i o` to be some unknown "foreign, side-effecting" computation which intakes values at type `i` and outputs them at type `o`. When we wrap `FFI i o` in `OI` we must pack it with a value for its input along with a "transformer" function which ensures that we're able to `fmap` over `FFI` [1]. The end result is that `OI` is a functor which packs up a potentially open type of external, side-effecting values.As a functor, we could `Free` it to get
data IO1 a where
Unit :: a -> IO1 a
Effect :: FFI i o -> i -> (o -> IO1 a) -> IO1 a
but Ed uses his interestingly optimized `Free` construct to instead give newtype IO2 a =
IO2 { runIO2 :: forall r .
(a -> r) ->
(forall i o . FFI i o -> i -> (o -> r) -> r) ->
r
}
This is a funky type, but you can think about what it's like to "interpret" IO2. Let's say you have a value of type `IO2 ()` and want to `interp` it io :: IO2 ()
interp :: IO2 () -> () -- clearly a side-effecting signature!
interp io = runIO2 io unit effect where
unit :: () -> ()
unit () = ()
effect :: FFI i o -> i -> (o -> ()) -> ()
effect ffi input continue =
What's neat here is that `effect` is essentially just asking you to give a semantics for the `FFI i o` abstract type. You have a value of it, you have its "input" and you have a continuation for its output. As the "side effecting interpreter" of `IO2`, you have free levity here to do whatever you need to "call" the `ffi` value. You also have free levity to "jump to another thread" before calling the continuation and passing control back to the author of the `IO2` value.Ed goes on to note that as far as giving semantics to `IO` inside of Haskell goes, there's a very simple choice for the abstract `FFI i o` type
type FFI i o = i -> o
getCharFFI :: FFI () Char
getCharFFI () = unsafePerformIO getChar
putCharFFI :: FFI Char ()
putCharFFI c = unsafePerformIO (putChar c)
-- etc.
[0] http://comonad.com/reader/2011/free-monads-for-less-3/
[1] This is Yoneda if you're into that kind of thingIt's also nice to do parallel threads in this way, but it's not infeasible to do them with the standard free formation. Totally just aesthetics. I'll see if I can dig up an example of two concurrent threads interacting over a shared flipflop.
Also worth thinking about a definition of FFI which goes like
data FFI i o where
Unsafe :: (i -> o) -> FFI i o
C :: (Storable i, Storable o) => String -> FFI i oUnless you are framework/lib programming, that is a phrase that should pretty much never be uttered.
[1] video: https://www.parleys.com/tutorial/composable-application-arch... | slides: https://dl.dropboxusercontent.com/u/4588997/ReasonablyPriced...
In the article case it is IO so if that ain't a framework, I don't know what is.