Home

Awesome

README


Build Status Version on Hackage

Rhine is a library for synchronous and asynchronous Functional Reactive Programming (FRP). It separates the aspects of clocking, scheduling and resampling from each other, and ensures clock-safety on the type level.

Recent breakage?

Confused because some examples from the article don't work anymore? Rhine went through a few bigger API simplifications and changes. If this broke your code, have a look at the versions readme to fix it.

Concept

Complex reactive programs often process data at different rates. For example, games, GUIs and media applications may output audio and video signals, or receive user input at unpredictable times. Coordinating these different rates is a hard problem in general. If not enough care is taken, buffer underruns and overflows, space and time leaks, accidental synchronisation of independent sub-systems, and concurrency issues, such as deadlocks, may all occur.

Rhine tackles these problems by annotating the signal processing components with clocks, which hold the information when data will be input, processed and output. Different components of the signal network will become active at different times, or work at different rates. If components running under different clocks need to communicate, it has to be decided when each component becomes active ("scheduling"), and how data is transferred between the different rates ("resampling"). Rhine separates all these aspects from each other, and from the individual signal processing of each subsystem. It offers a flexible API to all of them and implements several reusable standard solutions. In the places where these aspects need to intertwine, typing constraints on clocks come into effect, enforcing clock safety.

Example

A typical example, which can be run as cd rhine-examples/ && cabal run Demonstration, (or using nix flakes with nix develop followed cabal run Demonstration), would be:

  -- | Create a simple message containing the time stamp since initialisation,
  --   for each tick of the clock.
  --   Since 'createMessage' works for arbitrary clocks (and doesn't need further input data),
  --   it is a 'Behaviour'.
  --   @time@ is the 'TimeDomain' of any clock used to sample,
  --   and it needs to be constrained in order for time differences
  --   to have a 'Show' instance.
  createMessage
    :: (Monad m, Show (Diff time))
    => String
    -> Behaviour m time String
  createMessage str
    =   timeInfoOf sinceInit >-> arr show
    >-> arr (("Clock " ++ str ++ " has ticked at: ") ++)

  -- | Output a message /every second/ (= every 1000 milliseconds).
  --   Let us assume we want to assure that 'printEverySecond'
  --   is only called every second,
  --   then we constrain its type signature with the clock @Millisecond 1000@.
  printEverySecond :: Show a => ClSF IO (Millisecond 1000) a ()
  printEverySecond = arrMCl print

  -- | Specialise 'createMessage' to a specific clock.
  ms500 :: ClSF IO (Millisecond 500) () String
  ms500 = createMessage "500 MS"


  ms1200 :: ClSF IO (Millisecond 1200) () String
  ms1200 = createMessage "1200 MS"

  -- | Create messages every 500 ms and every 1200 ms,
  --   collecting all of them in a list,
  --   which is output every second.
  main :: IO ()
  main = flow $
    ms500 @@ waitClock            --  a Rhine = a ClSF in the context of a Clock
    |@|                           --  compose 2 Rhines in parallel
    ms1200 @@ waitClock           --  a Rhine at a different clock
    >-- collect -->               --  buffer results from both Rhines into a list
    printEverySecond @@ waitClock --  the final Rhine

  -- | Uncomment the following for a type error (the clocks don't match):

  -- typeError = ms500 >>> printEverySecond

This repository

Learn Rhine

The recommended way to start is the https://github.com/turion/rhine-koans/ tutorial. It leads you through basic and advanced Rhine concepts by solving many self-contained puzzles.

For a quick reference of the most important functions, operators, and concepts, see the cheatsheet.

Documentation resources

FAQ

Clocks must be the only things that block a thread, not ClSFs. So for example, you can fix:

arrMCl readLn

by using:

tagS >>> arr read :: ClSF IO StdinClock () Int

tagS contains the string that the StdinClock grabbed from stdin, and only the clock has been allowed to block the thread!

Yes, for instance you could implement a distance-dependent collision detector.

Several strategies exist and it depends on your use case. For FixedStep clocks, it won't matter since the execution of the program isn't tied to a realtime clock. For ClSFs running on UTCTime clocks, you can execute the slow code in a separate thread and coordinate merging the results back into the signal network.

Development

See Contributing.md for details.

Related projects