8 ms·
Monadic IO: Laziness Makes You Free
- tel 12y agoIn Haskell the IO monad is a weird beast. In particular, other than it "being a monad" and a couple of other instances it's not entirely clear what its semantics are. The "Free monad semantics" described in this post are one choice that is often used but it's a little bit tricky. Free monads are, in particular, "fixed" structures while `IO` can be extended using FFI. There are also questions about how to properly represent parallel threads in this way. 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/ http://comonad.com/reader/2011/free-monads-for-less-3/ [1] This is Yoneda if you're into that kind of thing
- noelwelsh 12y agoWow, epic comment. Thanks for this. I'm going to take a while to digest it.
- tel 12y agoHaha, well, free monads are incredibly interesting! Feel free to ask me any questions you have as well. I'd be more than happy to respond to email as well :)
- tome 12y agoIt sounds like you're claiming a free monad approach couldn't handle an FFI. I don't think that's right.
- tel 12y agoEh, I'm playing fast and loose to validate a number of interesting things that come out of that blog post. The `FFI a` functor is just Yoneda of (F o = exists i . (FFI i o, i)) which you want because we can't guarantee that `FFI i` is a functor. You can form the standard free monad over (Yoneda F) just fine, but if you do it in the weird CPS style indicated here you get something that's faster in Haskell ("well, ok, so what") but also has a really nice story where the only remaining bit is "the interpreter is exactly giving semantics to FFI". It'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 o
- weego 12y agoI’ve already hinted we need a monad Unless you are framework/lib programming, that is a phrase that should pretty much never be uttered.
- dllthomas 12y agoCould you expand a bit?
- noelwelsh 12y agoIt's very hard to intuit your point from this brief comment. Expanding on it would be interesting (I almost certainly disagree but I welcome informed discussion).
- whateveracct 12y agoOr just architecting something in general. This talk [1] proposes a way to approach application architecture that will result in the programmers writing their own monads (or rather, their own FREE monads) [1] video: https://www.parleys.com/tutorial/composable-application-architecture-reasonably-priced-monads https://www.parleys.com/tutorial/composable-application-arch... | slides: https://dl.dropboxusercontent.com/u/4588997/ReasonablyPriced.pdf https://dl.dropboxusercontent.com/u/4588997/ReasonablyPriced...
- bbcbasic 12y agoBut but... A monad is a framework :-) In the article case it is IO so if that ain't a framework, I don't know what is.
- serve_yay 12y agoI guess I'm just slow, but I don't understand how this isn't just moving the toothpaste around the tube, so to speak. You start out with an impure `println` and replace it with an impure `Pure.println`. 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.
- noelwelsh 12y agoPure.println is pure up until you call run -- because it doesn't actually do anything until that point. In the Example, Example.io gives the structure of the computation and Example.run actually runs it. It's this separation that allows monadic IO to be pure (until it is run). 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.
- serve_yay 12y agoWell, OK. You end up with an impure thunk from Pure.println, Pure.println itself is pure.
- innguest 12y agoThe difference is that after println, the next line of code runs in a world where the previous println happened. After a Pure.println, the next line of code does not run in a world where the previous println happened, because it didn't and can be revoked at any point in time by later actions, affecting the ultimate outcome of the program (so that maybe that particular Pure.println call never prints).
- evincarofautumn 12y agoIt doesn’t really work in Scala, at least not as this article formulates it. The basic idea in Haskell is that as far as the type system is concerned, we only ever compose impure actions from smaller ones. We never actually run them ourselves; the Haskell runtime does that. This means that we can pass around impure actions as values and manipulate them however we like. The Monad typeclass simply lets us write functions that are overloaded to work on many different kinds of actions, not just I/O.
- eranation 12y agoSo, I'm trying to explain this to myself in laymen terms, can you let me know how far is my naive understanding? 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)
- tel 12y agoIt's definitely SOC. You're pretty much correct in your interpretation, too. You "purify" the core of your code by asking it to merely construct a data structure which represents an impure program. When that pure core code produces the data structure your "run-time system" outside of it executes the program and produces side effects. 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.
- Rusky 12y agoYou're essentially correct. IO is there to mark functions that perform IO, to make it easier to separate them from the pure functions. 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.
- pron 12y agoTo me this seems like an unnecessary abstraction in 99% of the cases that then makes reasoning about execution so much harder. Once you go monadic IO, you have to use it everywhere (at least when dealing with a specific API), and then what? Suppose you wish to trace calls to println (or any other monadic operation) with Byteman (one of the greatest JVM tracing tools) by capturing all calls to the actual println (because, say, you want to know what operation resulted in a line printed to the log). You get a useless stacktrace that you can no longer pinpoint in the code. Monads erase all context. 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.
- chriswarbo 12y agoConsider an analogy to booleans. We can get a boolean in many different ways ('true'/'false' constants, numeric comparisons, etc.). We can only use a boolean in one way: selecting the branch of an if/then/else construct. All logical connectives, (de)serialisers, conversions, etc. and everything other 'users' of booleans that a language may have built-in can all be replaced using combinations of if/then/else. More formally, we could say that booleans have many introduction rules but only one elimination rule (if/then/else)[1]. 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_and_elimination http://en.wikipedia.org/wiki/Natural_deduction#Introduction_... [2] http://en.wikipedia.org/wiki/Church_encoding#Church_Booleans http://en.wikipedia.org/wiki/Church_encoding#Church_Booleans [3] https://existentialtype.wordpress.com/2011/03/15/boolean-blindness/ https://existentialtype.wordpress.com/2011/03/15/boolean-bli...