Hacker News new | ask | show | jobs
by endgame 3926 days ago
> wrap I/O while preserving functional purity

So I think that expanding your idea of what monads do might help. Monads enforce a sequencing, and let later computations in the sequence depend on the result of an earlier one. Have a look at the definition of the Monad instance for Maybe:

    instance  Monad Maybe  where
        return x            = Just x
        (Just x) >>= k      = k x
        Nothing  >>= _      = Nothing
Now consider (because it's a contrived but simple example) that you have some `Map` type (from Strings to Strings, just for convenience), a value of that type `myMap :: Map` and a function `lookup :: Map -> String -> Maybe String`. Let's do the equivalent of `myMap[myMap[myMap["a"]]]`:

    case lookup myMap "a" of
      Nothing -> Nothing
      Just v -> case lookup myMap v of
                  Nothing -> Nothing
                  Just v' -> lookup myMap v'
This pattern of 1. do a thing, 2. check its result, 3. feed the result into the next step of the computation is what's abstracted over by the monad. We can write the same lookup using `do`-notation:

    do
      v <- lookup myMap "a"
      v' <- lookup myMap v
      lookup myMap v'
If this is making sense, I'd suggest repeating the exercise with `Either`, which is often used to pass an error message on its `Left` constructor (bypassing the rest of the computation). Actual results are stored on the `Right` constructor. If that makes sense, then I would then look at `Reader` (which lets you do computations with some value (like an environmental context) at-hand, and then maybe `State`.

If all the functional stuff is clear, then I'd look at the `STM` monad, which implements Software Transactional Memory. STM is IO-like in the sense that you are manipulating shared state, but you only have a restricted set of tools to do it with - the type `STM a` means "a transaction that fiddles with some shared memory, then returns a value of type `a`". To actually execute the transaction, you have to turn it into an `IO` action using the function `atomically :: STM a -> IO a` and put it in a side-effecting computation somewhere.

Hopefully that clears things up a bit: `IO` is just a special case of this sequencing strategy, but the really cool things happen because we can define what sequencing computations means for different data types. I think this is what some haskellers mean when they say "monads let you overload semicolons".