The Idea
Hello! I'm trying to implement in Haskell an image processing library based on dataflow ideology. I've got a problem connected to how I want to handle the flow of control.
The main idea is to introduce a time
. The time
is a Float
, which could be accessed anywhere in the code (you can think of it like about State monad, but a little funnier). The funny thing about it, is that we can use timeShift
operation on results, affecting the time corresponding operations would see.
An example would be best to explain this situation. Lets use following dataflow diagram:
-- timeShift(*2) --
-- / \
-- readImage -- addImages -> out
-- \ /
-- blur ----------
and its pseudocode (which deos not typecheck - its not important if we use do or let notation here, the idea should be clear):
test = do
f <- frame
a <- readImage $ "test" + show f + ".jpg"
aBlur <- blur a
a' <- a.timeShift(*2)
out <- addImage aBlur a'
main = print =<< runStateT test 5
The 5
is the time
we want to run the test
function with. The timeShift
function affects all the operations on the left of it (in the dataflow diagram) - in this case the function readImage
would be run twice - for both branches - the lower one would use frame 5
and the upper one frame 5*2 = 10
.
The problem
I'm providing here a very simple implementation, that works great, but has some caveats I want to solve. The problem is, that I want to keep the order of all IO operations. Look at the bottom for example, which will clarify what I mean.
Sample implementation
Below is a sample implementation of the algorithm and a code, which constructs following dataflow graph:
-- A --- blur --- timeShift(*2) --
-- \
-- addImages -> out
-- /
-- B --- blur --------------------
the code:
import Control.Monad.State
-- for simplicity, lets assume an Image is just a String
type Image = String
imagesStr = ["a0","b1","c2","d3","e4","f5","g6","h7","i8","j9","k10","l11","m12","n13","o14","p15","q16","r17","s18","t19","u20","v21","w22","x23","y24","z25"]
images = "abcdefghjiklmnoprstuwxyz"
--------------------------------
-- Ordinary Image processing functions
blurImg' :: Image -> Image
blurImg' img = "(blur " ++ img ++ ")"
addImage' :: Image -> Image -> Image
addImage' img1 img2 = "(add " ++ img1 ++ " " ++ img2 ++ ")"
--------------------------------
-- Functions processing Images in States
readImage1 :: StateT Int IO Image
readImage1 = do
t <- get
liftIO . putStrLn $ "[1] reading image with time: " ++ show t
return $ imagesStr !! t
readImage2 :: StateT Int IO Image
readImage2 = do
t <- get
liftIO . putStrLn $ "[2] reading image with time: " ++ show t
return $ imagesStr !! t
blurImg :: StateT Int IO Image -> StateT Int IO Image
blurImg img = do
i <- img
liftIO $ putStrLn "blurring"
return $ blurImg' i
addImage :: StateT Int IO Image -> StateT Int IO Image -> StateT Int IO Image
addImage img1 img2 = do
i1 <- img1
i2 <- img2
liftIO $ putStrLn "adding images"
return $ addImage' i1 i2
timeShift :: StateT Int IO Image -> (Int -> Int) -> StateT Int IO Image
timeShift img f = do
t <- get
put (f t)
i <- img
put t
return i
test = out where
i1 = readImage1
j1 = readImage2
i2 = blurImg i1
j2 = blurImg j1
i3 = timeShift i2 (*2)
out = addImage i3 j2
main = do
print =<< runStateT test 5
print "end"
The output is:
[1] reading image with time: 10
blurring
[2] reading image with time: 5
blurring
adding images
("(add (blur k10) (blur f5))",5)
"end"
and should be:
[1] reading image with time: 10
[2] reading image with time: 5
blurring
blurring
adding images
("(add (blur k10) (blur f5))",5)
"end"
Please note that the correct output is ("(add (blur k10) (blur f5))",5)
- which means, that we added image k10
to f5
- from respectively 10th and 5th frame.
Further requirements
I'm looking for a solution, which would allow users to write simple code (like in test
function - it could of course be in a Monad), but I do not want them to handle the time-shifting logic by hand.
Conclusions
The only difference is the order of IO actions execution. I would love to preserve the order of the IO actions just like they are written in the test
function. I was trying to implement the idea using Countinuations
, Arrows
and some funny states, but without success.
Dataflow and functional reactive programming libraries in Haskell are usually written in terms of
Applicative
orArrow
. These are abstractions for computations that are less general thanMonad
s - theApplicative
andArrow
typeclasses do not expose a way for the structure of computations to depend on the results of other computations. As a result, libraries exposing only these typeclasses can reason about the structure of computations in the library independently of performing those computations. We will solve your problem in terms of theApplicative
typeclassApplicative
allows a library user to make new computations withpure
, operate on existing computations withfmap
(fromFunctor
) and compose computations together with<*>
, using the result of one computation as an input for another. It does not allow a library user to make a computation that makes another computation and then use the result of that computation directly; there's no way a user can writejoin :: f (f a) -> f a
. This restriction will keep our library from running into the problem I described in my other answer.Transformers, free, and the ApT transformer
Your example problem is quite involved, so we are going to pull out a bunch of high level Haskell tricks, and make a few new ones of our own. The first two tricks we are going to pull out are transformers and free data types. Transformers are types that take types with a kind like that of
Functor
s,Applicative
s orMonad
s and produce new types with the same kind.Transformers typically look like the following
Double
example.Double
can take anyFunctor
orApplicative
orMonad
and make a version of it that always holds two values instead of oneFree data types are transformers that do two things. First, given some simpler property of the underlying type the gain new exciting properties for the transformed type. The
Free
Monad
provides aMonad
given anyFunctor
, and the freeApplicative
,Ap
, makes anApplicative
out of anyFunctor
. The other thing "free" types do is they "free" the implementation of the interpreter as much as possible. Here are the types for the freeApplicative
,Ap
, the freeMonad
,Free
, and the free monad transfomer,FreeT
. The free monad transformer provides a monad transformer for "free" given aFunctor
Here's a sketch of our goal - we want to provide an
Applicative
interface for combining computations, which, at the bottom, allowsMonad
ic computations. We want to "free" the interpreter as much as possible so that it can hopefully reorder computations. To do this, we will be combining both the freeApplicative
and the free monad transformer.We want an
Applicative
interface, and the easiest one to make is the one we can get for "free", which aligns nicely with out goal of "freeing the interpeter" as much as possible. This suggests our type is going to look likefor some
Functor
f
and anya
. We'd like the underlying computation to be over someMonad
, andMonad
s are functors, but we'd like to "free" the interpreter as much as posssible. We'll grab the free monad transformer as the underlying functor forAp
, giving usfor some
Functor
f
, someMonad
m
, and anya
. We know theMonad
m
is probably going to beIO
, but we'll leave our code as generic as possible. We just need to provide theFunctor
forFreeT
. AllApplicatives
areFunctors
, soAp
itself could be used forf
, we'd write something likeThis gives the compiler fits, so instead we'll mover the
Ap
inside and defineWe'll derive some instances for this and discuss its real motivation after an interlude.
Interlude
To run all of this code, you'll need the following. The
Map
andControl.Concurrent
are only needed for sharing computations, more on that much later.Stuffing it
I mislead you in the previous section, and pretended to discover
ApT
from resoning about the problem. I actually discoveredApT
by trying anything and everything to try to stuffMonad
ic computations into anApplicative
and be able to control their order when it came out. For a long time, I was trying to solve how to implementmapApM
(below) in order to writeflipImage
(my replacement for yourblur
). Here's theApT
Monad
transformer in all its glory. It's intended to be used as theFunctor
for anAp
, and, by usingAp
as its ownFunctor
forFreeT
, can magically stuff values into anApplicative
that shouldn't seem possible.It could derive even more instances from
FreeT
, these are just the ones we need. It can't deriveMonadTrans
, but we can do that ourselves:The real beauty of
ApT
is we can write some seemingly impossible code likeThe
m
on the outside disappeares, even intoAp
that's merelyApplicative
.This works because of the following cycle of functions, each of which can stuff the output from the function above it into the input of the function below it. The first function starts with an
ApT m a
, and the last one ends with one. (These definitions aren't part of the program)This lets us write
And some utility functions for working on a transformer stack
We'd like to start writing our example image processors, but first we need to take another diversion to address a hard requirement.
A hard requirement - input sharing
Your first example shows
implying that the result of
readImage
should be shared betweenblur
andtimeShift(*2)
. I take this to mean that the results ofreadImage
should only be computed once for each time.Applicative
isn't powerful enough to capture this. We'll make a new typeclass to represent computations whose output can be divided into multiple identical streams.We'll make a transformer that adds this capability to existing
Applicative
sAnd provide some utility functions and instances for it
Image processors
With all of our transformers in place, we can start writing our image processors. At the bottom of our stack we have our
ApT
from an earlier sectionThe computations need to be able to read the time from the environment, so we'll add a
ReaderT
for thatFinally, we'd like to be able to share computations, so we'll add out
LetT
transformer on top, giving the entire typeIP
for our image processorsWe'll read images from
IO
.getLine
makes fun interactive examples.We can shift the time of inputs
Add multiple images together
And flip images pretending to use some library that's stuck in
IO
. I couldn't figure out how toblur
a string...Interpreting LetT
Our
LetT
for sharing results is at the top of our transformer stack. We'll need to interpret it to get at the computations underneath it. To interpretLetT
we will need a way to share results inIO
, whichmemoize
provides, and an interpeter that removes theLetT
transformer from the top of the stack.To share computations we need to store them somewhere, this
memoize
s anIO
computation inIO
, making sure it happens only once even across multiple threads.In order to interpret a
Let
, we need an evaluator for the underlyingApT IO
to incorporate into the definitions for theLet
bindings. Since the result of computations depends on the environment read from theReaderT
, we will incorporate dealing with theReaderT
into this step. A more sophisticated approach would use transformer classes, but transformer classes forApplicative
is a topic for a different question.Interpreting ApT
Our interpreter uses the following
State
to avoid needing to peek insideAsT
,FreeT
, andFreeF
all the time.Interpereting
Ap
is harder than it looks. The goal is to take data that's inAp.Pure
and put it inInPure
and data that's inAp
and put it inInAp
.interpretAp
actually needs to call itself with a larger type each time it goes into a deeperAp
; the function keeps picking up another argument. The first argumentt
provides a way to simplify these otherwise exploding types.interperetApT
gets data out ofApT
,FreeT
, andFreeF
and intoState m
With these simple interpreting pieces we can make strategies for interpreting results. Each strategy is a function from the interpreter's
State
to a newState
, with possible side effect happening on the way. The order the strategy chooses to execute side effects in determines the order of the side effects. We'll make two example strategies.The first strategy performs only one step on everything that's ready to be computed, and combines results when they are ready. This is probably the strategy that you want.
This other strategy performs all the calculations as soon as it knows about them. It performs them all in a single pass.
Many, many other strategies are possible.
We can evaluate a strategy by running it until it produces a single result.
Executing the intepreter
To execute the interpreter, we need some example data. Here are a few interesting examples.
The
LetT
interpreter needs to know what evaluator to use for bound values, so we'll define our evaluator only once. A singleinterpretApT
kicks off the evaluation by finding the initialState
of the interpreter.We'll compile
example2
, which is essentially your example, and run it for time 5.Which produces almost the desired result, with all reads happening before any flips.
A
Monad
can not reorder the component steps that make upimg1
andimg2
inif there exists any
m [i]
whose result depends on a side effect. AnyMonadIO m
has anm [i]
whose result depends on a side effect, therefore you cannot reorder the component steps ofimg1
andimg2
.The above desugars to
Let's focus on the first
>>=
(remembering that(>>=) :: forall a b. m a -> (a -> m b) -> m b
). Specialized for our type, this is(>>=) :: m [i] -> ([i] -> m [i]) -> m [i]
. If we are going to implement it, we'd have to write something likeIn order to do anything with
f
, we need to pass it an[i]
. The only correct[i]
we have is stuck insideimg1 :: m [i]
. We need the result ofimg1
to do anything withf
. There are now two possibilities. We either can or can not determine the result ofimg1
without executing its side effects. We will examine both cases, starting with when we can not.can not
When we can not determine the result of
img1
without executing its side effects, we have only one choice - we must executeimg1
and all of its side effects. We now have an[i]
, but all ofimg1
s side effects have already been executed. There's no way we can execute any of the side effects fromimg2
before some of the side effects ofimg1
because the side effects ofimg1
have already happened.can
If we can determine the result of
img1
without executing its side effects, we're in luck. We find the result ofimg1
and pass that tof
, getting a newm [i]
holding the result we want. We can now examine the side effects of bothimg1
and the newm [i]
and reorder them (although there's a huge caveat here about the associative law for>>=
).the problem at hand
As this applies to our case, for any
MonadIO
, there exists the following, whose result can not be determined without executing its side effects, placing us firmly in the can not case where we can not re-order side effects.There are also many other counter examples, such as anything like
readImage1
orreadImage2
that must actually read the image fromIO
.