Rhine is an arrowized FRP library with type level clocks. It is designed with multi-rate FRP in mind, combining synchronous arrowized FRP features (as in Yampa) with asynchronous building blocks.
Building blocks in Rhine
There are two kinds of separation of concerns: First, temporal aspects (clocking/sampling) are separated from dataflow aspects. Second, synchronous components are separated from asynchronous ones. The situation is summarised in this table:
|Synchronous||Clocks||Clocked signal functions|
In Rhine, you can annotate each (synchronous) signal function with a clock type. This clock type gives clock safety since throughout the framework it is ensured that every signal function is run at the speed of this clock.Rhine has a number of ready-to-use clocks, for example fixed-rate clocks like
Signal functions are aware of time and internal state. Standard arrowized FRP constructions are implemented in Rhine:
type Pos = (Double, Double, Double) freeFallFrom10Meters :: ClSF m cl a Pos freeFallFrom10Meters = arr (const (0,0,-9.81)) >>> integral >>> integralFrom (0,0,10)
m is an arbitrary monad and
cl is an arbitrary clock type.)
Being based on [Dunai],
signal functions can also perform side effects in a monad
exitWhenNegative :: ClSF (ExceptT () m) cl Pos () exitWhenNegative = throwOn' <<< arr (\(_, _, z) -> (z < 0, ()))
Like in standard Haskell, monad transformers need to be handled. In Rhine (and Dunai), interesting things happen when handling monad transformers. In the case of
ExceptT, we have control flow!
freeFallThenOnTheGround :: ClSF m cl a Pos freeFallThenOnTheGround = safely $ do try $ freeFallFrom10Meters >>> (id &&& exitWhenNegative) >>> arr fst safe $ arr $ const (0, 0, 0)
This little signal function simulates an object that falls from 10 meters, throws an exception when the height drops below 0, and then eternally stays at the coordinates
(0, 0, 0). This example is discussed at length in the article (link on the bottom of the page).
We can also use other monads, like
printHeight :: ClSF IO cl a () printHeight = freeFallThenOnTheGround >>> arrMCl print
Rhine does not hide any side effects under the hood. All effects that are used are visible in the type.
Signal functions are "clocked" and can only be composed synchronously when their clocks match. The following will not typecheck:
doubleAt1Hz :: ClSF m (Millisecond 1000) Integer Integer doubleAt1Hz = arr (* 2) doubleAt2Hz :: ClSF m (Millisecond 500) Integer Integer doubleAt2Hz = arr (* 2) quadrupleAtNonsensicalRate = doubleAt1Hz >>> doubleAt2Hz
* Couldn't match type `500' with `1000'
Behaviours and Events
- Rhine is developed on https://github.com/turion/rhine. The development version usually stays up to date with the latest GHC. Support requests for applications developed with Rhine are very welcome there.
- Rhine is on stackage.
General development guideline
To build an application with Rhine, you can usually follow this guideline:
- Ask yourself what the different synchronous subsystems of the whole application should be. You will recognise them by the rate they work ("tick") at. For example in a game, you might have a user event system, a physical simulation, and a graphics system.
- Ask yourself whether you have any special clocks, e.g. external devices, event machines you need to connect to, external loops you have to connect to. Implement those as clocks in Rhine, ideally using existing components.
- Implement each synchronous subsystem as a clocked signal function.
- Decide how to schedule the different clocks. In most cases, you will be able to use deterministic or concurrent schedules from the library.
- Decide how to resample the data from one subsystem to the other. Take existing resampling buffers from the library, or build your own buffers from existing ones and signal functions.
- Build the main Rhine program from all the components. The clock types will tell you whether you have correctly combined everything.
- Run the main loop and possibly add initialisation and clean up actions.
- The main article: https://www.manuelbaerenz.de/article/rhine-frp-type-level-clocks
- For an example app, see https://github.com/turion/sonnendemo
- Haskell Symposium 2018 presentation by Iván Pérez: To be uploaded