5 ms·
I 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
by serve_yay 11y ago
I 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 11y 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 11y agoWell, OK. You end up with an impure thunk from Pure.println, Pure.println itself is pure.
- innguest 11y 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 11y 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.
- implicit 11y agoI use this technique to achieve bulletproof unit tests in Haskell HTTP services by providing two run functions: One that runs on IO and does all the real stuff, and another that twiddles some state and produces a pure result. This nets us tests that are 100% reliable. The compiler rejects all code that can introduce external inputs.
- JackMorgan 11y agoDo you have an example demonstrating that pattern? I'd love to see it.
- virmundi 11y agoI'm with you about the general idea and I'm not a pure functional programmer (just getting into Clojure). I've often thought of this before and I do appreciate the answers to your inquiry. 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. 1 -http://rationalwiki.org/wiki/Noble_lie http://rationalwiki.org/wiki/Noble_lie
- serve_yay 11y agoIt does seem that way. I can't really understand the value of that, typically we want our models to model reality and not lie to us.
- noelwelsh 11y agoImagine you have to organise a large group of people to do some complicated job. You give everyone very explicit instructions (computers are better at following instructions than people, but imagine that these people are good at this). Nobody does anything until you've finished giving instructions. Then you shout "go!" and everything happens. 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.
- tel 11y agoOne thing that might highlight the value here is to use a reduced set of effects. Here's (in Haskell, sorry) the "teletype monad" -- 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.
- Guvante 11y ago> but it returns a thunk which is not. 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.
- wyager 11y agoIt's worth noting that monads don't necessarily execute strictly or in order. The details of execution are up to the monad implementation.