Having tried (more or less successfully) to find good introductory literature on the topic of Functional Reactive Programming (FRP) I decided to keep track of my own experiments and convert the resulting log to small tutorials. Take this as a disclaimer: I myself am trying to figure out how this all works. Any ideas how to do better are thus more than welcome.
Functional Reactive Programming is a technique to work with systems that change over time using the toolbox of functional programming. The idea is to treat the whole system as a “stream” which stems from some input, in between sits the programmer who is to decide how to use the input stream and create from it an output stream. Consider the following examples:
A GUI application
It is easily possible to interpret GUI applications as an instance of the reactive programming perspective:
- The input signal shall be the user inputs. Consider the case in which the only means of input is a mouse with just one button, then the signal is a tuple
of the current coordinates
and
and a boolean
indication whether or not the mouse button is currently pressed.
- The output signal is what is drawn on the screen. This might for example be a simple pixel array with RGB values for each point.
- The task of the programmer is to couple the input signal to the output signal. As an example, we probably want to move the graphical cursor on the screen according to the current
position. This means that the corresponding pixels in the pixel array are changed accordingly.
A Robot Controller
A different use case is a controller of a robotic system. Imagine for example an arm with a number of joints to control. The fact that this is a system that exhibits some “behaviour over time” indicates that again FRP is applicable. And indeed:
- The input signal will be the information we get from the sensors, measuring e.g. joint positions.
- The output signal is whatever is sent to the robot actuators e.g. motor torques.
- The task of the programmer is to map from the sensor data to the actuator signal.
FRP Frameworks
There are several Haskell frameworks that support FRP. Unfortunately I find it difficult to estimate their respective level of maturity and overall quality. I will be focusing on Yampa which can be installed quite easily using cabal:
$ cabal install Yampa
The Haskell Wiki list some more FRP frameworks but I found the documentation rather sparse.
The “Hello World” Example
Let’s start with a simple “Hello World” application. The idea is to achieve the following: The input to the system is static signal with value “Hello Yampa”. This signal will be piped through the main program without being changed. Thus, the resulting signal should be just “Hello Yampa”. All the output function has to do is to print it via putStrLn and indicate that it wants to terminate the program.
import FRP.Yampa main :: IO () main = reactimate initialize input output process initialize :: IO String initialize = return "Hello Yampa" input :: Bool -> IO (DTime, Maybe String) input _ = return (0.0, Nothing) output :: Bool -> String -> IO Bool output _ x = putStrLn x >> return True process :: SF String String process = identity
Let’s see bit by bit what happens here.
The type of the reactimate
function, which serves as the core of the application is as follows:
reactimate :: IO a -> (Bool -> IO (DTime, Maybe a)) -> (Bool -> b -> IO Bool) -> SF a b -> IO ()
Basically, the type argument a
is the type of the input signal. In our case this will thus simply be a string. The first argument is the initial input data. As it is in the IO
Monad, we could retrieve this data from the actual physical system (e.g. the position of the mouse cursor).
The second argument of reactimate
is the function that supplies us with sensor data. I do not understand why it takes a Bool
parameter and according to this page, that parameter is not even used. Again, it is wrapped in the IO
Monad and we could use it to get the new position of the mouse cursor or the current sensor data. Let’s look at the type of the thing that is wrapped in IO
: It is (DTime, Maybe a)
. The first part of the tuple is the amount of time that has passed since the last time the input
function has been called. It is thus (in theory) our task to do the bookkeeping and to return the right value here. However, the way the Hello Yampa program works, input
will actually never be called, thus the exact return value does not matter. The second element can be Nothing
which translates to “there was no input”.
The return value of the output
function signals whether or not we want to terminate: True
stops the program. As in this simple example the return value is always True
the program will terminate immediately after having executed output
once.
More information about reactimate
can be found here.
Timed “Hello World”
Obviously, printing “Hello Yampa” on a screen would not be the example were FRP would typically be applied. Instead, we want to deal with systems that change over time. To include this time element, let us consider a slightly extended “Hello World”-example in which
- the input consists of a constant stream of “Hello Yampa” and the current time
- the function is to filter all instances where the time is more than two seconds
- the output is to print the result to console as before
import FRP.Yampa import Data.IORef import Data.Time.Clock type Input = (String, DTime) type Output = Maybe String data Stopwatch = Stopwatch { zero :: UTCTime prev :: UTCTime } startStopwatch :: UTCTime -> Stopwatch startStopwatch now = Stopwatch now now storeStopwatch :: IO (IORef Stopwatch) storeStopwatch = getCurrentTime >>= (newIORef . startStopwatch) diffTime :: (IORef Stopwatch) -> IO (DTime,DTime) diffTime ref = do now <- getCurrentTime (Stopwatch zero prev) <- readIORef ref writeIORef ref (Stopwatch zero now) let dt = realToFrac (diffUTCTime now prev) timePassed = realToFrac (diffUTCTime now zero) return (dt, timePassed) main :: IO () main = do handle <- storeStopwatch reactimate initialize (input handle) output process initialize :: IO Input initialize = return ("Hello Yampa",0) input :: (IORef Stopwatch) -> Bool -> IO (DTime, Maybe Input) input ref _ = do (dt,timePassed) <- diffTime ref return (dt, Just ("Hello Yampa",timePassed)) output :: Bool -> Output -> IO Bool output _ (Just x) = putStrLn x >> return False output _ Nothing = return True process :: SF Input Output process = arr afun where afun (message, timePassed) | timePassed <= 1 = Just message | otherwise = Nothing
The first thing to notice here is the code that keeps track of the time that has passed. The start time and the time of the last call of input
are stored in an IORef
reference. I wonder why this kind of very generic looking code is not part of the reactimate loop.
The input
function now returns a tuple containing the string and the time that has passed. Furthermore it updates the time of the stopwatch. The process
function looks at the time that has passed and decides to either return Nothing
or to just pipe through the message.
Using the arrow syntax for which we need
{-# LANGUAGE Arrows #-}
this can be written this in a different way:
process :: SF Input Output process = proc (message, timePassed) -> returnA -< if (timePassed <= 1) then Just message else Nothing
I suppose that the arrow syntax does not really make sense in this simple case. Whenever the process
function returns Nothing
the program will stop.
In theory, the signal function is assumed to be continuous. Therefore we should get infinitely many instances of “Hello Yampa” printed to the screen. Obviously that’s not possible. And indeed, internally Yampa seems to discretize the signal function.
Conclusion
There are some things that puzzle me about Yampa. For instance:
- There are those seemingly unused boolean parameters in the input and output functions.
- I don’t quite understand why the whole time-bookkeeping work is not done internally.
Yet I think that these two examples provide some intuition about how a Yampa program is structured. Also it becomes apparent what the beauty of this approach is to a Haskell programmer: All unpure operations can be located in the input
and output
function. The process
that translates input to output has no side effects at all. Now I have to look at some more sophisticated examples involving state.
Updates
- kick-starter for the Reactive Banana library. : Alfredo simultaniously worked on a
Nothing
in theinput
function actually corresponds to “there was no input”. I updated the text accordingly.
: Gerold noted below that a value of
I like your loud thinking!
> The second element can be Nothing which translates to “use the same input value as last time”.
Nothing translates to ‘there was no input’ 🙂
> There are those seemingly unused boolean parameters in the input and output functions.
I think they were originially supposed to serve a purpose, but now are left as code artefacts. just ignore them or ask in Yampa mailing list.
> I don’t quite understand why the whole time-bookkeeping work is not done internally.
The bookkeeping actually is, I think you refer to the “elapsed time”. This is simply because you want to seperate the concern of “FRP infrastructure” and “how the time differences are produced”. You are simply using Data.Time.Clock, I always used a fixed amount of “60”, but we might as well use high-precision timer or external time givers.
Thanks for your comments. I updated the text to include your remark that “Nothing translates to ‘there was no input'”.
However, It seems that if there is no new input, Yampa will interpolate and use the last input as the new input to the signal function, right?
I’m not quite sure what you mean by that.
input :: … -> IO (DTime, Maybe Input)
If you mean “DTime”, take a look at the integral SF. this is a very fundamental signal function which encapsulates time as a state. It uses Euler integration and if DTime would be 0, time wouldn’t progress (but I think this is prohibited). If you mean “Maybe Input”, than Nothing really is Nothing. You’re code has to provide handling of this case, so there cannot be anything to interpolate.