While hacking something up earlier, I created the following code:
newtype Callback a = Callback { unCallback :: a -> IO (Callback a) }
liftCallback :: (a -> IO ()) -> Callback a
liftCallback f = let cb = Callback $ \x -> (f x >> return cb) in cb
runCallback :: Callback a -> IO (a -> IO ())
runCallback cb =
do ref <- newIORef cb
return $ \x -> readIORef ref >>= ($ x) . unCallback >>= writeIORef ref
Callback a represents a function that handles some data and returns a new callback that should be used for the next notification. A callback which can basically replace itself, so to speak. liftCallback just lifts a normal function to my type, while runCallback uses an IORef to convert a Callback to a simple function.
The general structure of the type is:
data T m a = T (a -> m (T m a))
It looks much like this could be isomorphic to some well-known mathematical structure from category theory.
But what is it? Is it a monad or something? An applicative functor? A transformed monad? An arrow, even? Is there a search engine similar Hoogle that lets me search for general patterns like this?
The term you are looking for is free monad transformer. The best place to learn how these work is to read the “Coroutine Pipelines” article in issue 19 of The Monad Reader. Mario Blazevic gives a very lucid description of how this type works, except he calls it the “Coroutine” type.
I wrote up his type in the
transformers-freepackage and then it got merged into thefreepackage, which is its new official home.Your
Callbacktype is isomorphic to:To understand free monad transformers, you need to first understand free monads, which are just abstract syntax trees. You give the free monad a functor which defines a single step in the syntax tree, and then it creates a
Monadfrom thatFunctorthat is basically a list of those types of steps. So if you had:That would be a syntax tree that accepts zero or more
as as input and then returns a valuer.However, usually we want to embed effects or make the next step of the syntax tree dependent on some effect. To do that, we simply promote our free monad to a free monad transformer, which interleaves the base monad between syntax tree steps. In the case of your
Callbacktype, you are interleavingIOin between each input step, so your base monad isIO:The nice thing about free monads is that they are automatically monads for any functor, so we can take advantage of this to use
donotation to assemble our syntax tree. For example, I can define anawaitcommand that will bind the input within the monad:Now I have a DSL for writing
Callbacks:Notice that I never had to define the necessary
Monadinstance. BothFreeT fandFree fare automaticallyMonads for any functorf, and in this case((->) a)is our functor, so it automatically does the right thing. That’s the magic of category theory!Also, we never had to define a
MonadTransinstance in order to uselift.FreeT fis automatically a monad transformer, given any functorf, so it took care of that for us, too.Our printer is a suitable
Callback, so we can feed it values just by deconstructing the free monad transformer:The actual printing occurs when we bind
runFreeT callback, which then gives us the next step in the syntax tree, which we feed the next element of the list.Let’s try it:
However, you don’t even need to write all this up yourself. As Petr pointed out, my
pipeslibrary abstracts common streaming patterns like this for you. Your callback is just:The way we’d define
printerusingpipesis:… and we can feed it a list of values like so:
I designed
pipesto encompass a very large range of streaming abstractions like these in such a way that you can always usedonotation to build each streaming component.pipesalso comes with a wide variety of elegant solutions for things like state and error handling, and bidirectional flow of information, so if you formulate yourCallbackabstraction in terms ofpipes, you tap into a ton of useful machinery for free.If you want to learn more about
pipes, I recommend you read the tutorial.