1. 11
  1.  

  2. 2

    I wish that I understood monads in Haskell well enough to appreciate this article - but I’ve had so much trouble trying to grasp how monadic IO and state work under-the-hood that I’ve never gotten around to learning about the different ways that they can be used.

    In particular I remember reading these paragraphs from two different articles:

    From Unraveling the mystery of the IO monad

    “When we teach beginners about Haskell, one of the things we handwave away is how the IO monad works. Yes, it’s a monad, and yes, it does IO, but it’s not something you can implement in Haskell itself, giving it a somewhat magical quality.”

    And from Pure IO monad and Try Haskell

    “As Haskellers worth their salt know, the IO monad is not special … I’d recommend Haskell intermediates (perhaps not newbies) to implement their own IO monad as a free monad, or as an mtl transformer, partly for the geeky fun of it, and partly for the insights.”

    These inconsistent descriptions of how much magic these monads involve is, for me, a far bigger source of confusion than notation.

    1. 4

      Here’s a short explanation of what’s “going” under the hood. It’s shortly a problem of representing interactions using functions.

      You could think that under every “print”, etc. there’s a function that takes a world and gives a world where something’s been printed to your screen. Now you can chain these functions to do input/output.

      But how about you write a function that fills a type such as World → (World, World)? There’s a trick to preventing this. Lets say that you must return a function World → World, and the result you return determines which interaction is actually happening when you run the program, you can still create “speculated” images.

      But there’s still a problem, how about you do this:

      let (x, w1) = read (print "input?" world)
      in print ("hello " ++ x) world
      

      To make it clear, this program is producing it’s output from results that cannot be produced because they are result of speculation.

      It’s possible to limit production of results such that you can only read those results that you actually chained to being part of the interaction. This is achieved by retrieving the result in a function. Eg. you get the result by passing in: input → World. Now the system can prepend the interaction to be part of the result and you no longer can use up values that end up being pure speculation.

      “Under the hood” the value you returned is deconstructed and steps are taken to produce the interaction that’s represented.

      If you need more details, I can provide, but eventually they become implementation dependent. The first article you got is probably slightly wrong because it conflates things. You can roll your own IO monad inside Haskell.

      {-# LANGUAGE GADTs #-}
      
      module MY_IO where
      
      data MyIo a where
          M_print :: String -> MyIo ()
          M_getLine :: MyIo String
          M_bind :: MyIo a -> (a -> MyIo b) -> MyIo b
      
      m_hello :: MyIo ()
      m_hello = M_getLine `M_bind` step2
          where step2 name = M_print ("Hello " ++ name)
      
      m_interpret :: MyIo a -> IO a
      m_interpret (M_print s) = print s
      m_interpret (M_getLine) = getLine
      m_interpret (M_bind x f) = m_interpret x >>= m_interpret . f
      

      You can also consider how it works if you replace “data MyIo” by “class MyIo”.

      Think that it’s not real IO monad because “it’s been implemented with IO”. Well.. You don’t need to implement it with IO.

      l_interpret :: MyIo a -> [String]
          -> Either [String] (a, [String], [String])
      l_interpret (M_print s) x      = Right ((), x, [s])
      l_interpret (M_getLine) (x:xs) = Right (x, xs, []) 
      l_interpret (M_getLine) []     = Left []
      l_interpret (M_bind x f) xs = case l_interpret x xs of
          Left ys1 -> Left ys1
          Right (z,xs1,ys1) -> case l_interpret (f z) xs1 of
              Left ys2 -> Left (ys1 ++ ys2)
              Right (q,xs2,ys2) -> Right (q, xs2, ys1 ++ ys2)
      

      For example, there it’s been interpreted as abstract interactions like that. And now you can examine the “m_hello” as potential interactions:

      *MY_IO> l_interpret m_hello []
      Left []
      *MY_IO> l_interpret m_hello ["foo"]
      Right ((),[],["Hello foo"])
      *MY_IO> l_interpret m_hello ["foo", "bar"]
      Right ((),["bar"],["Hello foo"])
      

      You can also treat it with continuations if you don’t like that the previous thing recomputes everything when you add an input.

      1. 2

        Most people tell me to just use them in Haskell in a variety of situations to understand them rather than worrying about what’s underneath. I still searched for a specific comment on internals that enlightened me quite a bit. I didn’t find it.

        I’ll still share the search results since they had many interesting comments on the topic. One or more might be helpful. One even has an implementation of Maybe in the C language.