3 ms·
In 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 semantic
by tel 12y ago
In 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