Abstract
The Functional reactive programming provides a declarative solution to the task of creation of interactive applications. Often it is contrasted to the imperative approach that relies on callback handlers, event loops and mutation of the state which is quickly can become hard to understand and maintain. In this work we show how with imperative definition we can build DSL that covers all combinators of the FRP approach. We will go to the center of imperative hurricane to build declarative interface that is easy to understand, use and extend. Because of imperative nature of the definitions it simplifies the creation of bindings to imperative systems as they speak the same language at the core of the library. Also we show application of our approach to various domains such as Animation, gaming, UIs, server-side web programming.
Introduction
Working with time is a hard task, because everything starts to depend on context of execution and order of calls to procedures. One of the main merits of the functional programming and pure functions is that time does not interfere with execution of the program. Pure functions produce the same result regardless of when they are called. Effectful procedures tend to update mutable state and it is hard to track the updates. As a solution to this problem FRP treats the whole sequence of events that trigger updates as a single entity and we can establish relationships between streams of events. And work with them with convenient interface that looks as if we work with lists.
By treating the event streams as a whole we surpass the problem of event ordering
because we process the infinite stream of events as a whole. But this introduces
its own problems. We need to be careful to not to introduce the space leaks because
some events that should be forgotten are kept in memory. Also it can be hard to
bridge the imperative and declarative approaches because at the end of the day often we
use imperative frameworks as low-level implementation of FRP ideas. Take for example OpenGL
or WxWidgets
which are inherently imperative.
In this paper we propose an implementation for FRP system which is imperative at its core. We use powerful Haskell features such as lazy evaluation and concurrency to implement FRP combinators on top of imperative definitions. And we make the imperative world composable and to the end user it seems that the whole model is declarative and reactive.
Because implementation is imperative at the core it is easy to implement bindings to such a system and core types a very easy to grasp and use as example of FRP implementation for education.
Our main contributions:
-
Minimal and elegant implementation for FRP which is based on imperative approach.
-
Use of concurrency in the FRP system
-
Generic monad at the core of the FRP system
-
Implementation of bindings to several domains: animation (
gloss
,processing-for-haskell
), UIs (brick
), web-servers (scotty
)
It was implemented in the library dyna
. It is open source and available on Hackage.
The name comes as a short name for “dynamic value”.
FRP refresher
In this section we provide a short overview of functional reactive programming (FRP). It is a declarative approach to creation of interactive applications. It studies continuous processes (called Behaviors or Signals) and infinite streams of events (called Events). Instead of reacting to the user input and updating the current state we use typical functional programming routines like map, fold, filter, monoidal interface to establish relationships between user input and drawings of widgets on the screen.
We have two main types: Behaviours or continuous signals and event streams.
By the meaning the Behavior can be though of continuous function from Time
to values:
type Behavior a = Time -> a
And event stream is possibly infinite list of events, i.e. values that happen on certain time:
type Events a = [(Time, a)]
The magic starts to happen when we bring typical functional programming functions to the domain of FRP. We use interface for FRP as it was designed in []:
Typical interface for Behaviors
:
instance Functor Behavior
instance Applicative Behavior
From the Applicative
instance we can derive many useful instances that lift
some instances of the values to the instances of the signals:
instance Monoid a => Monoid (Behabior a)
instance Num a => Num (Behavior a)
instance Fractional a => Fractional (Behavior a)
instance IsString a => IsString (Behavrior a)
...
The interface for event streams also can be expressed with standard type-classes.
We can map over values of events with Functor
instance Functor Events where
We can join several event streams together with instance of Monoid
:
instance Semigroup (Event a) where
instance Monoid (Event a) where
It is important that we can join events regardless of their type. Whereas for Behaviors
we required that parameter of Behavior is also Monoid
. Conceptually we can think on join
of events as of merge of two lists of events which are sorted by time of event occurrence.
Also event streams support operations typical for lists:
filter :: (a -> Bool) -> Events a -> Events a
take :: Int -> Events a -> Events a
drop :: Int -> Events a -> Events a
scan :: (s -> a -> s) -> s -> Events a -> Events s
... etc ...
We need some way to interact between events and behaviors:
stepper :: a -> Events a -> Behavior a
apply :: Behavior (a -> b) -> Events a -> Events b
The stepper function creates step-wise behavior that starts with pure
initial value (the first argument) and when event happens on the event stream (second argument)
it starts to produce that value instead until the next event will happen and overwrite the current
value. The apply
function applies the dynamic function to the value of event stream when it happens.
The result stream is going to have the same timestamps for events as the input stream
only we are going to map with function which also dynamically changes over time.
We can switch between behaviors and events:
switchE :: Events (Events a) -> Events a
switchB :: Behavior a -> Events (Behavior a) -> Behavior a
The switchE
flattens the event streams. It starts to produce the events
from the first event stream that happens until the next event stream will come and
substitute the previous one and so forth.
In the switchB
we flatten the event stream of behaviors to a single behavior.
It works just like stepper
only we work not with constant values within the segments
but with behaviors.
On top of those basic functions we can construct many more primitives but this
is the core of FRP system. There are many ways to implement FRP. Mostly they are
divided on classical FRP which implements described interface and Arrowised FRP (AFRP) which
introduce the notion of signal function. It is a function which can be thought of
as iterative transformation of the Behavior
.
We propose imperative implementation that is based on the notion of callback procedure. It turns out that we can create flexible and powerful set of combinators that cover the whole range of FRP interface with that. But prior to that let us take a broad view on the problem and study the meaning of the FRP entities from the philosophical perspective.
Imperative definition
What does it mean to be a continuous process or stream of events from the user point of view? We have just outlined one possible solution described in types:
type Behavior a = Time -> a
type Events a = [(Time, a)]
It introduces the relation of behaviors and streams to a time. We assume that there is a time line in which we observe the events or values of behaviors.
What if we can eliminate the time completely? If instead of thinking about what event is we can think about why do we need it in the first place. This is kind of categorical view on the problem when we study not what objects are but how they are related to other objects.
For our implementation we assume that user needs event stream to react on events and it does not matter when event will happen as long as we know that some procedure is going to be called in that moment.
We can picture it:
We define the event stream of a
in terms of function runEvt
which
takes in a callback procedure a -> m ()
and does something
useful with it whenever event happens. The m
is some monad
that is akin to IO
. But when does it going to happen
is completely hidden from the user.
Also we can think of behaviors in terms of how they can be used in real world. Take for example the current temperature of the weather. We can try to forecast it but the only reliable thing we can do is to measure it in the current time. For us behavior is kind of reference to the mutable process that evolves in background and the only thing we can do is to query the current value of the process.
readDynRef :: DynRef m a -> m a
Also as the dynamic value is a background process we need some way to gracefully shut it down:
cancelDyn :: DynRef m a -> m ()
We are going to call behaviors of this kind dynamic values (Dyn
for short in the code)
If we have some definition of the process for dynamic value we can run it in background
and get the reference to it:
runDyn :: Dyn m a -> m (DynRef m a)
So we have two imperative definitions:
The event stream Evt
is a callback consumer function:
newtype Evt m a = Evt { runEvt :: (a -> m ()) -> m () }
And the dynamic value is some background process to which we have access over reading its current value. We can read it only at the current moment:
runDyn :: Dyn m a -> m (DynRef m a)
readDyn :: DynRef m a -> m a
cancelDyn :: DynRef m a -> m ()
We need to refine the notion of Dyn
. What type of process is it?
We define it in terms of event streams. It is observation of current value
of some underlying event stream.
We are going to have some initial value, event stream and getter function:
data Dyn m a = forall s . Dyn
{ dyn'init :: s
, dyn'evt :: Evt m s
, dyn'get :: s -> m a
}
So at the beginning of the process we observe the initial value dyn'init
and produce (dyn'get dyn'init
) until the next event will happen. We store it
in the mutable value (it can be IORef
for example). When it will happen we will
update the mutable value and start to produce another value.
We use trick with forall
to hide away the internal representation of the state
from the user. The implementation in the library also contains a method:
dyn'release :: m ()
It holds procedure to release resources allocated for the work of event stream dyn'evt
but we omit it for the sake of simplicity.
At the core of our system is a notion of event stream as callback consumer. The dynamic value is an observation of some event stream. It turns out that with this imperative definition we can implement all functions from the classical FRP interface.
Let us start with interfaces for event streams. As we go we will encounter
constraints for the monad m
.
Event stream interface
Event streams are the core of our system as dynamic values are defined in terms of event streams. The vent streams are callback consumer functions. The combinators for them are going to take the input callback functions and append some useful functionality on top of them.
Event stream constructors
The simplest possible event stream is never
. It never happens
and the implementation just ignores the callback and returns:
never :: Monad m => Evt m a
never = Evt $ \_ -> pure ()
The next basic primitive is once
. It is some event that happens only once
right at the start:
once :: Monad m => m a -> Evt m a
once act = Evt $ \go -> go =<< act
It just invokes the callback once and returns. For example with once
we can create an event stream that queries user for input:
getLineE :: Evt IO a
getLineE = once getLine
To print the events we can define useful functions that calls print
as a callback:
printE :: Show a => Evt IO a -> IO ()
printE evt = runEvt evt print
Sequential composition
With our definition of events we can introduce sequential composition. As we define our event stream as monadic procedure it makes it simple to define the function that implements one monadic action after another one. It is when we have two event streams that produce the events of the same type we can run the first one with our callback and when it finishes we can the second one:
seqE :: Monad m => Evt m a -> Evt m a -> Evt m a
seqE a b = Evt $ \go -> do
runEvt a go
runEvt b go
Note how the nature of monad being a sequence of procedures produces very simple definition for sequencing of event streams. With this function we can ask user for input several times:
getLineE `seqE` getLineE
Also using the same idea it is easy to provide the foreverE
function that calls event stream in infinite loop. We just lift
the standard function for monads forever
to the event streams:
foreverE :: Monad m => Evt m a -> Evt m a
foreverE e = Evt $ \go -> forever (runEvt e go)
Note how sequential composition does not rely on the notion of time. It is defined in terms of one goes after another one finishes but introduction of the time is still avoided.
Parallel composition
With concurrent libraries we can define the parallel composition of event streams. Two event streams happen concurrently if they run at the same time and we pass the same callback to them and whenever event happens on any of the streams it triggers the callback procedure:
parE :: MonadBaseControl m => Evt m a -> Evt m a -> Evt m a
parE a b = Evt $ \go -> concurrently_ (runEvt a go) (runEvt b go)
We use the function concurrently_
from the library async-lifted
to
run both event streams at the same time.The class MonadBaseControl
let us run processes concurrently not only on top of IO
monad
but on any monad that is instance of it. It gives us great flexibility
over the choice of the underlying monad.
Frp-monad class
For the sake of brevity we define the Frp
-class for a monad
that is suitable for using with Evt
, Dyn
and FRP interface:
class MonadBaseControl m => Frp m where
Monoid instance
With never
we can define Monoid either with sequential composition
or with parallel composition. In FRP libraries the Monoid instance
usually means parallel composition:
instance Frp m => Semigroup (Evt m a) where
(<>) = parE
instance Frp m => Monoid (Evt m a) where
mempty = never
Functor instance
It is also useful to be able to transform event streams. For example if we want to read only integer input from the user:
readInt :: String -> Maybe Int
readIntE :: Evt IO (Maybe Int)
readIntE = foreverE (fmap readInt getLineE)
We assume the existence of the function readInt
that reads only integers
from the input.
In the definition of the Functor
we just apply the function to transform
the input prior to callback invocation:
instance Frp m => Functor (Evt m) where
fmap f e = Evt $ \go -> runEvt e (go . f)
List-like functions
What makes the definition of applications in FRP-style great is that many combinators look like functions for ordinary list. We work with event streams as if they are lists although under the hood thy are functions with side effects. Let us look at couple of definitions.
Filtering
Let’s define some list-like functions. Let us take for example filterE
:
filterE :: Frp m => (a -> Bool) -> Evt m a -> Evt m a
filterE cond e = Evt $ \go ->
runEvt e $ \x -> when (cond x) (go x)
Once again we don’t need the notion of time to filter the events.
We just ride on top of the monad function when
that can skip
execution of monadic actions based on condition.
Scanning
The scan function accumulates the state with some function whenever event happens on the stream:
scan :: (a -> st -> st) -> st -> Evt m a -> Evt m st
This function is a bit tricky. As to implement it we are going to need the mutable
variables. We need to store the current state and update it on the event.
For simplicity we take the IORef
to store the state but the implementation also
provides TVar
to store values more safely in the presence of
possible concurrent updates.
scan f init e = Evt $ \go -> do
ref <- liftIO (newIORef evt s)
runEvt evt $ \x -> do
s <- f x <$> liftIO (readIORef ref)
go s
liftIO $ writeIORef ref s
We allocate a mutable reference under the hood and update it on every trigger
of the callback function. And this is all hidden from the user behind
the declarative interface of function scan
.
In this manner we can implement many list functions. But we stop for now and resume with interface for dynamics.
Rock Paper Scissors game
We would like to provide the feel of usage of FRP library that we have just defined. As example we list implementation of the game of Rock Paper Scissors. In the game two players show random hand gestures to each other. There are three gestures: Rock, Paper and Scissors. And Paper wins over Rock, Rock wins over Scissors and Scissors win over Paper. If gestures are the same it is a draw.
It takes just a 7 lines of FRP-code to implement the logic of the game
and uses only event stream functions.
Here is the complete code for the FRP-part of the Rock-Paper-Scissors game.
The pipe operator |>
is just flipped $
application operator:
data Move = Rock | Paper | Scissors
data Score = Score {...}
toScore :: Move -> Move -> Score -- convert moves to score for a single round
printScore -- pretty prints the score
:: RoundId -> Score -> String
total :: Int -- number of rounds
game :: Evt IO String
game =
once getLine |> foreverE |> -- read user input
mapMay (readMaybe @Move) |> -- take only valid moves
takeE total |> -- take only so many moves
withOneOf [Rock, Paper, Scissors] |> -- add random AI move as first element of the tuple
foldMapE (uncurry $ flip toScore) |> -- get the score and accumulate scores
withCount |> -- append the round number
fmap (printScore total) -- print the current score
The steps are explained in the comments. We just create the infinite
stream of user input and filter only valid moves. After that we take
only so many moves as it’s needed by the game. We append to the user’s
move the random move of the game-engine and foldMap
it with conversion
to the score. The function withCount
appends the current count of
the events so far. As the last stage we pretty print the scores on
each round.
Interface for dynamic values
We have defined the interface for the core type - event streams. Let us dive into dynamic values. We can recall that dynamic values are just event streams in disguise. We turn the streams to processes by observation of the last event value that has happened on the stream.
Let us reminder the definition:
data Dyn m a = forall s . Dyn
{ dyn'init :: s -- ^ initial state
, dyn'evt :: Evt m s -- ^ event stream that updates the state
, dyn'get :: s -> m a -- ^ getter function for the state
}
Basic building blocks
We can create a dynamic from an effectful value:
constDyn :: Monad m => m a -> Dyn m a
constDyn a = Dyn
{ dyn'init = ()
, dyn'evt = never
, dyn'get = const a
}
It uses the event that never happens to update the value.
And the getter function dyn'get
just uses the input to produce the value.
Also the definition of dynamic value lends itself to the construction of dynamic values from event streams:
hold :: Monad m => a -> Evt m a -> Dyn m a
hold init evt = Dyn
{ dyn'init = init
, dyn'evt = evt
, dyn'get = pure
}
For dynamics we need to define only couple of interfaces: Functor
and Applicative
.
Let’s start with the Functor
.
Functor instance
The Functor
case is simple we just need to change our point of view
on the returning value with getter function:
instance Fynctor m => Functor (Dyn m) where
fmap f (Dyn init evt get) = Dyn init evt (fmap f . get)
Applicative functor instance
Let’s define first an easy case for pure
:
instance Frp m => Applicative (Dyn m) where
pure a = constDyn (pure a)
The bind of applicative functor is a bit tricky to get right because it implies the construction of single event stream for internal state update out of two argument streams. The main idea is to use monoid instance to run two event streams concurrently and use a pair of states as a result state. And whenever any of the components updates we also update the internal pair of sates:
(<*>) :: Dyn m (a -> b) -> Dyn m a -> Dyn m b
(<*>) (Dyn initF evtF getF) (Dyn initA evtA getA) = Dyn init evt get
where
init = (initF, initA)
evt = Evt $ \go -> runEvt (fmap Left evtF <> fmap Right evtA) $ \(f, a) -> case x of
Left f' -> go (f', a)
Right a' -> go (f, a')
get (f, a) = pure (f a)
We have defined the main functions for dynamic values!
Interaction between events and dynamics
Let’s define application of the dynamic function to the event stream. To do that we start to listen for dynamic values and each time the callback is triggered on input stream we just read the dynamic function by reference and apply it to callback argument:
apply :: Frp m => Dyn m (a -> b) -> Evt m a -> Evt m b
apply dyn evt = Evt $ \go -> do
ref <- runDyn dyn
runEvt evt (\b -> do
go . ($ b) =<< readDyn ref)
`finally` cancelDyn ref
We need to be careful to stop the background process
when we do not need it anymore. For that we use the finally
primitive which is going to be called even if the exception
will be thrown in the thread of the event stream.
The implementation for flattening functions swithE
and switchB
is tricky story and we list it in the appendix for the interested reader.
Where the time is
It is interesting that we still do not need to use the notion of time.
How should we interact with it? We can introduce it with
primitive which calls a procedure on the given time interval.
Assume that we have predefined function preiodic
:
preiodic :: Monad m => Time -> m () -> m ()
It calls procedure every so many seconds. It is easy to implement it
with treadDelay
function which can wait in the thread.
With it we can define an event stream that calls callback periodically:
pulse :: Frp m => Time -> Evt m ()
pulse t = Evt $ \go -> periodic t (go ())
This is enough to sample the timeline with the granularity that we would
like to have. With this event stream we can start to work with time
but the library of FRP-combinators itself does not need it for implementation.
This separation of interface from the time allows us to keep decisions of
the granularity and preciseness of time orthogonal to the whole system.
More fruits from Concurrent Haskell
As we saw the application of a function concurrently_
from the asyn-lifted
library.
We can use other functions to give concurrent flavor to the execution
of event streams. For example we can also use race
:
raceE :: Frp m => Evt m a -> Evt m a -> Evt m a
raceE a b = Evt $ \go -> race_ (runEvt a go) (runEvt b go)
It will run both of the events concurrently and finish both of the event streams when one of them will finish. It can be useful to timeout the execution of event streams.
Also we can create a combinator that handles all callbacks concurrently by running each callback in a separate thread:
forkE :: Evt m a -> Evt m a
forkE e = Evt $ \go -> runEvt e (fork . go)
Recursion
It is often useful to use recursive event streams. For example we can have a button widget which has dynamic attribute text on the button and we would like to show how many times the button was pressed with it. The code can look like this:
clicks <- button (fmap show $ hold 0 $ count clicks)
Where count
counts how many times event has occurred on the stream.
So we need clicks as output and as input of the function.
We can not do it directly with our framework because event streams
are functions under the hood and we can not tie the knot over generic
function definition.
To solve this problem we introduce a combinator:
fixE :: (Evt m a -> m (Evt m a)) -> Evt m a
It takes in a function which transforms the event streams and produces
a single event stream. It feeds the output stream of the function back to itself.
With this combinator we can implement the button
:
fixE $ \clicks -> button (fmap show $ hold 0 $ count clicks)
To implement this function we need to introduce another useful concept.
Event streams which are based on concurrent channels.
We can initialise a channel (for example TChan
from the library stm
).
And in the event loop we can listen for messages and each time the message
occurs we are going to trigger the callback.
The definition for TChan
:
tchanEvt :: Frp m => TChan a -> Evt m a
tchanEvt chan = Evt $ \go -> do
ref <- liftIO $ atomically $ dupTChan chan
loop ref go
where
loop ref go = do
a <- liftIO $ atomically $ readTChan ref
go a
loop chan go
With it we can implement the fixE
. To redirect the events form output
to input we are going to create a channel and we will write all events
from the output to it and we will create an event stream from that channel
and apply it to the recursive function:
fixE :: Frp m => (Evt m a -> m (Evt m a)) -> Evt m a
fixE f = Evt $ \go -> do
chan <- liftIO newTChanIO
let evt = tchanEvt chan
evt' <- f evt
runEvt evt' $ \x -> do
liftIO $ atomically $ writeTChan chan x
go x
Sharing of event streams
We have defined a nice DSL of FRP combinators on top of our imperative definition of event streams. But it’s good to be aware of its limitations. Each event stream is an effectful function under the hood and we combine them to produce new functions that can produce side effects too. This means that we accumulate the description of execution of some callback rather than the execution itself.
It can lead to confusion with the usage of Haskell sharing of the variables
with let
or where
expressions. For example we have a function:
oneOf :: [a] -> Evt b -> Evt a
Which for every event in the second argument stream produces event that contains one random element from the list. If we use it in the code:
let a = oneOf [True, False] evt
b = a
in f a b
It’s useful to understand that a
and b
will produce two different
instances of the random process. More than that even in the expression:
let a = oneOf [True, False] evt
in f a a
In the function f
on execution of callback procedure two arguments
of f
will produce different random sequences although they point to the same
variable a
. But it is important to understand that a
contains a description
of the callback consumer and not the evaluation of it.
Often we want to share the values between streams and to solve this problem we introduce a special function:
newEvt :: Evt m a -> m (Evt m a)
It creates a concurrent channel and starts to execute its argument event stream with callback that writes values of events to the concurrent channel. As a result we return the event stream that listens to that channel.
This way if we copy this description we copy the listener to the same channel and both of the copies would receive the same events:
do
a <- newEvt (oneOf [True, False] evt)
pure (f a a)
In this expression the function f
will receive the same
events on execution from both a
s. In the library we have the
same function to share dynamic values.
Some FRP libraries solve this problem by usage of unsafePerformIO
to update the network of mutable references to the vent streams and keep sharing consistent
with the sharing of the Haskell values. We decided not to go down this road
and keep it explicit for the user.
Creation of bindings to imperative libraries
One of the strength of this approach to FRP implementation comes from
how easy it is to create bindings to imperative libraries.
Because the underlying types use imperative approach too.
The general technique is to use ReaderT
monad with
environment that contains concurrent channels or mutable variables
that are written from imperative evaluation loop of the imperative library.
Let’s outline the structure of bindings to the gloss
animation library.
It offers great declarative interface
for creation of animations but the main rendering loop functions
like an imperative state-machine.
Here is a simplified version of the function (we omit initialisation parameters):
playIO
:: world -- initial value of the state
-> (world -> IO Picture) -- render the state on the screen
-> (Event -> world -> IO world) -- update the state on events
-> (Float -> world -> IO world) -- update the state on interation step
-> IO ()
To use it with our FRP-library we create environment type which will hold the events:
newtype Env = Env { unEnv :: TChan Event }
And our main FRP-monad would be reader-transformer with access to that channel:
type Run a = ReaderT Env IO a
Our main rendering function will take a dynamic value of pictures as input:
runGloss :: Run (Dyn Run Picture) -> IO ()
The gloss
-level state of our application will be the reference to the dynamic value.
In the rendering on the screen we will just query the current value:
runGloss pictureDyn = do
env <- initEnv
ref <- runReaderT (runDyn pictureDyn) env
-- world == DynRef Run Picture
let draw ref = runReaderT (readDyn ref) env
In the rendering function we will initialise the environment and inside update of the events we will just dump all the events to the channel that is stored in the environment:
let update event ref = do
atomically $ writeTChan (unEnv env) event
pure ref
let frameUpdate _ ref = pure ref
With those functions we are ready to run the imperative gloss rendering function:
playIO draw update frameUpdate
That’s all we need to do. To provide the user with nice event stream of events we access the environment by the reader monad and wrap it to the event:
getEvents :: Evt Run Event
getEvents = Evt $ \go -> do
(Env eventChan) <- ask
runEvt (tchanEvt eventChan) go
To make user experience nicer in the real library we also created wrappers that settle down the generic monad parameter for event streams and dynamic values:
newtype Evt a = Evt (Dyna.Evt Run a)
newtype Dyn a = Dyn (Dyna.Dyn Run a)
So the signature of the previous example looks more easy to understand:
getEvents :: Evt Event
Applications
The same technique of keeping communication with imperative event loop
over concurrent channels works for many other libraries. We have created
bindings to animation libraries gloss
and processing-for-haskell
,
also bindings to TUI library brick
and for server side web-library scotty
.
Animations
Creation of animations for the first impulse to creation of FRP approach.
And indeed ti fits nicely to that domain. Let’s show the hello world program
for gloss
bindings to see how those ideas can be used in practice.
In the program we have a solid circle that is drawn in the mouse pointer:
main :: IO ()
main = runApp defSpec $ pure pic
pic :: Dyn Picture
pic = (\pos -> translate pos $ color green $ circleSolid 50) <$> mouse
Note how we use the Functor
instance to transform the dynamic value of
current mouse
pointer position to the pictures of circles.
And it produces the picture as dynamic value.
TUIs
We can create terminal user interfaces with brick
library and
to find out how FRP will work in that setting we have created a binding to it
which is called dyna-brick
. We have implemented a 15 puzzle game
and 2048 games to test how it works. And it was great experience.
The game becomes a scan over user moves which for both games
are just the arrow moves or Esc
or q
to quit and r
to restart the game.
To give a taste of implementation we can show the most interesting FRP parts of it. Here is a 15 puzzle
show me what you ve got
Web servers
Comparison to other FRP libraries
There are many FRP libraries in the Haskell ecosystem.
There is a league of libraries that implement classic FRP like reactive-banana
, reflex
or
frp-now
and also there is big family of libraries that implement Arrow-style FRP,
most notorious are Yampa
and Dunai
. As we see it the place of the dyna
library among
those is for educational reasons, because model is very simple to grasp and also
it’s imperative nature makes it a good fit to create bindings to imperative libraries.
Other nice properties is due to update in place we don’t have holding to the past problem and
space leaks that are connected with that another interesting property is that dyna
library is based on concurrency from the ground up.
It would be interesting to see how the concurrency strain of it will evolve
and if it become more prominent and affect the design choices for the applications
written in it.
Further work
We plan to implement more bindings. It would be interesting to create bindings
to wxWidgets
library to create desktop applications. As we already tested out the TUIs
with brick. It was easy to implement and start with it is interesting to see how this
approach will develop with Desktop applications which often asks for recursive definitions
of the widgets.
Another interesting direction is to try out the concept for the dynamic DOM applications.
The callback-style implementation requires from the host language to have mutable variables
and being able to create concurrent processes. We are interested to implement
the library in purescript
as it offers very lean and great to read javascript
output
comparing to ghcjs
compiler. We would like to try out our ideas with purescript
and see how far we can get with that as Javascript
offers concurrent workers
and there are works that make this feature available to purescript
.
Conclusion
In this paper we have discussed a novel and elegant approach to FRP implementation.
In this approach instead of hiding away the imperative logic from the user we use
it as a cornerstone of our library and expose internals. We have shown how to implement
the combinators for classical FRP and that it can be done in elegant and generic
manner with the help of powerful concurrency model of the Haskell language.
Being able to use generic concurrency routines from the libraries lifted-base
and async-lifted
is a cornerstone of our implementation and what makes it
easy to implement and extend.
We hope that this will inspire the FRP users to experiment with this approach for teaching and for real applications!