reactive-banana

repository·master·Indexed 19 days ago

https://github.com/heinrichapfelmus/reactive-banana

A Haskell library for Functional Reactive Programming (FRP) featuring a high-performance push-driven implementation for production (Reactive.Banana.Prim) and a model-based implementation for semantic testing (Reactive.Banana.Model). It utilizes core abstractions of Behaviors and Events, implemented via a three-layer model consisting of a low-level mutable graph, mid-level Pulse and Latch structures, and a high-level API using observable sharing and caching.

Tokens
1.4K
Snippets
3
Records
9
Agent score
19%

What's inside reactive-banana

  1. What are Behaviors and Events in reactive-banana?

    master

    The reactive-banana library implements Functional Reactive Programming (FRP) using two core abstractions:

    1. Behavior: Represents a value that varies continuously over time. It denotes the entire history and future of a value (e.g., a numerical display tracking a counter).
    2. Events: Represents a sequence of discrete moments in time that carry data (e.g., button clicks).

    In a declarative program, you specify how Events are translated into Behaviors. For example, a sequence of button clicks (Events ()) can be transformed into a counter (Behavior Int).

  2. Understand Observable Sharing and Caching

    master

    Observable Sharing

    Observable sharing is a technique used in reactive-banana to ensure that when you use a host language binding (like Haskell's let) to define an FRP primitive, the underlying computation is shared rather than duplicated.

    Without observable sharing, a definition like let e = filterE p event in (e, e) might result in the filter logic being executed twice. With observable sharing, the library detects that the same variable is being used and ensures the underlying Pulse or Latch is only constructed once.

    The Caching Mechanism

    In the implementation, this is achieved via the Cached type from Reactive.Banana.Prim.High.Cached.

    • Cached m a: Describes an action of type m a that is designed to be executed only once. Subsequent attempts to execute the same cached action will simply retrieve the previously computed result.
    • cache :: m a -> Cached m a: This function wraps an arbitrary action m a in a caching mechanism. It uses internal side effects (similar to unsafePerformIO and mutable references) to ensure the action's result is stored and reused.

    In reactive-banana, Events and Behavior are defined as Cached actions within the Moment monad:

    type Behavior a = Cached Moment (Latch a, Pulse ())
    type Events a   = Cached Moment (Pulse a)
  3. How Events and Behaviors are implemented

    master

    In reactive-banana, the high-level Events and Behavior types are implemented using a combination of Pulse, Latch, and a technique called observable sharing.

    • Events are essentially represented as Pulse a values.
    • Behaviors are represented as a pair: (Latch a, Pulse ()). The Latch holds the current value, and the Pulse () acts as a notification mechanism to signal when the behavior has been updated. This allows the library to efficiently connect behaviors to the outside world by only reacting to updates.

    Both types are wrapped in a Cached mechanism within the Moment monad to ensure that once an event or behavior is defined, its underlying structure is shared rather than duplicated.

  4. Understand the semantics of Behaviors and Events

    master

    When working with reactive-banana, keep the following semantic rules in mind:

    • Continuous Time for Behaviors: Behaviors use continuous time. There is no notion of an "update" for a Behavior; you cannot write programs that depend on the frequency of a Behavior's updates. This ensures that updates which do not change the value do not affect program logic.
    • Recursion: Mutual recursion between Behavior and Event is well-defined and allowed. This is often necessary for complex reactive logic.
  5. How reactive-banana is implemented: The three-layer model

    master

    The library is structured into three distinct layers to manage the complexity of push-based propagation and prevent time leaks:

    1. Low-level (Mutable Graph): A mutable graph that represents dependencies between Events and enables push-based information propagation. It uses a Vault for storage and handles garbage collection.
    2. Mid-level (Pulse and Latch): Uses data structures called Pulse (roughly corresponding to Events) and Latch (roughly corresponding to Behaviors). These are references to nodes in the low-level mutable graph. New nodes are created using the Build monad.
    3. High-level (Events and Behaviors): The public API. Event and Behavior are implemented in terms of Pulse and Latch. This layer uses observable sharing to map Haskell variables to mutable references, allowing impure internal updates to appear as pure functions to the user.
  6. Understand the reactive-banana FRP implementations

    master

    The reactive-banana library provides two distinct implementations of Functional Reactive Programming (FRP):

    1. Reactive.Banana.Model: A model implementation designed for testing and understanding the semantics of the library. This is useful for educational purposes and inspecting how the FRP logic behaves.
    2. Reactive.Banana.Prim: The efficient, push-driven implementation intended for production use. This is the high-performance engine used in real applications.
  7. Build and run GUI examples using wxHaskell

    master

    To run the GUI examples, you must first install the wx package. Note that this requires having the development version of the wxWidgets libraries installed on your system. If you encounter ExitFailure 1 errors, refer to the wxHaskell Quick Start instructions.

    1. Install the wx package:

      cabal install wx
    2. Build the wx examples:

      cd reactive-banana-wx
      cabal configure -fbuildExamples && cabal build
      cd ..
    cabal install wx
    
    cd reactive-banana-wx
    cabal configure -fbuildExamples && cabal build
    cd ..