ivanperez-keera / dunai

Classic FRP, Arrowized FRP, Reactive Programming, and Stream Programming, all via Monadic Stream Functions
197 stars 30 forks source link
abstraction arrows frp frp-library functional-programming functional-reactive-programming game games haskell haskell-library monad yampa
# Dunai [![Build Status](https://api.travis-ci.com/ivanperez-keera/dunai.svg?branch=develop)](https://app.travis-ci.com/github/ivanperez-keera/dunai) [![Version on Hackage](https://img.shields.io/hackage/v/dunai.svg)](https://hackage.haskell.org/package/dunai) Dunai is a **generalized reactive programming library** on top of which other variants like Classic FRP, Arrowized FRP and Reactive Values can be implemented. [Installation](#installation) • [Examples](#examples) • [Documentation](#documentation) • [Related projects](#related-projects) • [Technical information](#technical-information) • [Contributions](#contributions) • [History](#history)

Features

Table of Contents

Installation

(Back to top)

Pre-requisites

(Back to top)

To use Dunai, you must have a Haskell compiler installed (GHC). We currently support GHC versions 7.6.3 to 9.8.1. It likely works with other versions as well.

On Debian/Ubuntu, both can be installed with:

$ apt-get install ghc cabal-install

On Mac, they can be installed with:

$ brew install ghc cabal-install

Compilation

(Back to top)

Once you have a working set of Haskell tools installed, install Dunai by executing:

$ cabal update
$ cabal install --lib dunai

Running the following will print the word Success if installation has gone well, or show an error message otherwise:

$ runhaskell <<< 'import Data.MonadicStreamFunction; main = putStrLn "Success"'

Examples

(Back to top)

Open a GHCi session and import the main Dunai module:

$ ghci
ghci> import Data.MonadicStreamFunction

An MSF is a time-varying transformation applied to a series of inputs as they come along, one by one.

Use the primitive arr :: (a -> b) -> MSF m a b to turn any pure function into an MSF that applies the given function to every input. The function embed :: MSF m a b -> [a] -> m [b] runs an MSF with a series of inputs, collecting the outputs:

ghci> embed (arr (+1)) [1,2,3,4,5]
[2,3,4,5,6]

MSFs can have side effects; hence the m that accompanies the type MSF in the signatures of arr and embed. The function arrM turns a monadic function of type a -> m b into an MSF that will constantly apply the function to each input.

For example, the function print takes a value and prints it to the terminal (a side effect in the IO monad), producing an empty () output. Elevating or lifting print into an MSF will turn it into a processor that prints each input passed to it:

ghci> :type print
print :: Show a => a -> IO ()
ghci> :type arrM print
arrM print :: Show a => MSF IO a ()

If we now run that MSF with five inputs, all are printed to the terminal:

ghci> embed (arrM print) [1,2,3,4,5]
1
2
3
4
5
[(), (), (), (), ()]

As we can see, after all side effects, embed collects all the outputs, which GHCi shows at the end.

When we only care about the side effects and not the output list, we can discard it with Control.Monad.void. (Dunai provides an auxiliary function embed_ for the same purpose.)

ghci> import Control.Monad (void)
ghci> void $ embed (arrM print) [1,2,3,4,5]
1
2
3
4
5

MSFs can be piped into one another with the functions (>>>) or (.), so that the output of one MSF is fed as input to another MSF at each point:

ghci> void $ embed (arr (+1) >>> arrM print) [1,2,3,4,5]
2
3
4
5
6

A monadic computation without arguments can be lifted into an MSF with the function constM:

ghci> :type getLine
getLine :: IO String
ghci> :type constM getLine
constM getLine :: MSF IO a String

This MSF will get a line of text from the terminal every time it is called, which we can pipe into an MSF that will print it back.

ghci> void $ embed (constM getLine >>> arrM putStrLn) [(), ()]
What the user types, the computer repeats.
What the user types, the computer repeats.
Once again, the computer repeats.
Once again, the computer repeats.

Notice how we did not care about the values in the input list to embed: the only thing that matters is how many elements it has, which determines how many times embed will run the MSF.

Simulations can run indefinitely with the function reactimate :: MSF m () () -> m (), which is useful when the input to the MSFs being executed is being produced by another MSFs, like in the case above with constM getLine producing inputs consumed by arrM putStrLn:

ghci> reactimate (constM getLine >>> arr reverse >>> arrM putStrLn)
Hello
olleH
Haskell is awesome
emosewa si lleksaH
^C

Dunai has a very extensive API and supports many programming styles. MSFs are applicatives, so we can transform them using applicative style, and they are categories, so they can be piped into one another with Control.Category.(.). For example, the line above can also be written as:

ghci> reactimate (arrM putStrLn . (reverse <$> constM getLine))

which is equivalent to:

ghci> reactimate (arrM putStrLn . fmap reverse . constM getLine)

Other writing styles (e.g., arrow notation) are also supported. This versatility makes it possible for you to use the notation you feel most comfortable with.

MSFs are immensely expressive. With MSFs, you can implement stream programming, functional reactive programming (both classic and arrowized), reactive programming, and reactive values, among many others. The real power of MSFs comes from the ability to carry out temporal transformations (e.g., delays), to apply different transformations at different points in time, and to work with different monads. See the documentation below to understand how capable they are.

Documentation

(Back to top)

Publications

(Back to top)

The best introduction to the fundamentals of Monadic Stream Functions is:

The following papers are also related to MSFs:

Videos

(Back to top)

Related projects

(Back to top)

Games

(Back to top)

Libraries

Technical information

(Back to top)

Performance

(Back to top)

Simpler games will be playable without further optimisations. For example, the game haskanoid works well with Dunai/Bearriver. You can try it with:

$ git clone https://github.com/ivanperez-keera/haskanoid.git
$ cd haskanoid/
$ cabal install -f-wiimote -f-kinect -fbearriver

It uses unaccelerated SDL 1.2, the speed is comparable to Yampa's:

$ haskanoid
Performance report :: Time per frame: 13.88ms, FPS: 72.04610951008645, Total running time: 1447
Performance report :: Time per frame: 16.46ms, FPS: 60.75334143377886, Total running time: 3093
Performance report :: Time per frame: 17.48ms, FPS: 57.20823798627002, Total running time: 4841
Performance report :: Time per frame: 19.56ms, FPS: 51.12474437627812, Total running time: 6797
Performance report :: Time per frame: 19.96ms, FPS: 50.100200400801604, Total running time: 8793
Performance report :: Time per frame: 19.44ms, FPS: 51.440329218106996, Total running time: 10737

It runs almost in constant memory, with about 50% more memory consumption than with Yampa: 200k for Yampa and 300K for Dunai/Bearriver. (There is very minor leaking, probably we can fix that with seq.)

We have obtained different figures tracking different modules. In the paper, we provided figures for the whole game, but we need to run newer reliable benchmarks including every module and only definitions from FRP.Yampa, FRP.BearRiver and Data.MonadicStreamFunction.

Dunai includes some benchmarks as part of the main library. You are encouraged to use them to evaluate your pull requests, and to improve the benchmarks themselves.

Contributions

(Back to top)

If this library helps you, you may want to consider buying the maintainer a cup of coffee.

Discussions, issues and pull requests

(Back to top)

Discussions

If you have any comments, questions, ideas, or other topics that you think will be of interest to the Dunai community, start a new discussion here. Examples include:

Issues

If a specific change is being proposed (either a new feature or a bug fix), you can open an issue documenting the proposed change here.

If you are unsure about whether your submission should be filed as an issue or as a discussion, file it as a discussion. We can always move it later.

Pull requests

Once we determine that an issue will be addressed, we'll decide who does it and when the change will be added to Dunai. Even if you implement the solution, someone will walk you through the steps to ensure that your submission conforms with our version control process, style guide, etc. More information on our process is included below.

Please, do not just send a PR unless there is an issue for it and someone from the Dunai team has confirmed that you should address it. The PR is very likely to be rejected, and we really want to accept your contributions, so it will make us very sad. Open a discussion / issue first and let us guide you through the process.

Structure and internals

(Back to top)

This project is split in three parts:

Dunai also includes some benchmarks as part of the main library. You are encouraged to use them to evaluate your pull requests, and to improve the benchmarks themselves.

Style

(Back to top)

We follow this style guide.

Version control

(Back to top)

We follow git flow. In addition:

See the recent repo history for examples of this process. Using a visual repo inspection tool like gitk may help.

Versioning model

(Back to top)

The versioning model we use is the standard in Haskell packages. Versions have the format <PUB>.<MAJOR>.<MINOR>(.<PATCH>)? where:

History

(Back to top)

This library Dunai was created by Ivan Perez and Manuel Baerenz. It is named after the Dunai (aka. Danube, or Дунай) river, one of the main rivers in Europe, originating in Germany and touching Austria, Slovakia, Hungary, Croatia, Serbia, Romania, Bulgaria, Moldova and Ukraine.

Other FRP libraries, like Yampa and Rhine, are named after rivers. Dunai has been chosen due to the authors' relation with some of the countries it passes through, and knowing that this library has helped unite otherwise very different people from different backgrounds.