3 ms·
So, 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 de
by eranation 12y ago
So, 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.
- GeneralMayhem 12y agoIt's separation of concerns, yes, but that's only one half of it. The other half is that the relationship between those concerns is defined in a way that can be abstracted to apply to a whole host of other context-like properties (state, nullability, atomicity, lazy thunk execution, etc) and allow them to be manipulated uniformly. The monad laws allow operations over the monadic value to be handled exactly the same as pure values in cases where you don't care about the context, which is an extremely powerful abstraction. 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.
- chousuke 12y agoNot quite ELI5, but Haskell tries to make IO pure by essentially representing all side-effecting programs as data and separating evaluation from execution. 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.
- drostie 12y agoI have to comment to specifically highlight this comment above. You can easily understand monadic I/O when you understand chousuke's comment; until you understand that comment you will have trouble understanding how Haskell does I/O. 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.
- T-R 12y agoThat's part of it. For IO/State, the idea is basically, if you wite a function that does something, you have to test it by doing that thing, so, instead, let's write recipes to do things, and then run them (recipes here just being sets of instructions - not trying to make a weird kitchen metaphor, just seemed more clear than "build computations"). Then, the code to build the recipes can be pure, and we can maybe do neat things like check recipes for equality, change up how we run them to do practice runs while taking notes, pass the recipes around and modify them, and build up libraries of not just recipes, but recipe modifications. A nice side effect is that it lends itself really well to separation of concerns/testable code - if your language is type-checked, it enforces it. If not, it's kind of optional, but there's not much reason not to keep it pure, since you're effectively doing dependency injection already. It's useful for non-side-effecty things, too - for State monads without side-effects, our "recipe" is a state machine, and it means we're being explicit about our inputs, which also makes it easy to do stuff like lookahead - changing the state and then dropping back to a previous one. 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".