Effective ATS:
Functional Reactive Programming via Bacon.js

Bacon.js is a small JS library for supporting functional reactive programming (FRP). There is currently a fairly limited API in ATS for a subset of functions in Bacon.js. In this article, I would like to present some basic examples in ATS involving FRP via Bacon.js.

A Simple Counter

Let me first contrast FRP with the direct style of reactive programming based on callbacks. This is particularly relevant for someone unfamiliar with FRP.

Suppose that we want to implement a counter that is associated with three buttons of the names: up, down and reset. One implemenation is given as follows, which makes direct use of three callback functions (that is, one for each button):


00



By clicking the Reset button, one resets the displayed count to 0. By clicking the Up/Down button, one increases/decreases the displayed count by 1. However, the count cannot go below 0 or above 99.

Such a counter can certainly be thought of as an object with an encapsulated state, and the callback functions are just some methods associated with the object. For instance, the reference theCount in the following (partial) implementation of a counter represents the state of the counter and the two functions are methods for updating this state:

//
local

val
theCount = ref{int}(0)

in (* in-of-local *)

implement
theCounter_button_up_click
  ((*void*)) = let
  val () =
  theCount[] :=
  min(theCount[]+1, 99) in theCount_update()
end // end of [theCounter_button_up_click]

implement
theCounter_button_down_click
  ((*void*)) = let
  val () =
  theCount[] :=
  max( 0, theCount[]-1) in theCount_update()
end // end of [theCounter_button_down_click]

end // end of [local]
//
Note that the function theCount_update is called for updating the displayed text (representing the state of the counter). The entirety of this callback-based counter implementation is available on-line.

A Simple Counter of FRP-style

In functional reactive programming (FRP), a button is modeled as a stream of events (to be generated when the button is clicked), and there are a large variety of functions in Bacon.js for creating and manipulating such streams of events. Note that the notion of event stream in Bacon.js is not the same as the notion of stream in ATS for supporting stream-based lazy-evaluation.

Given a type T, the type EStream(T) is for an event stream in which each item is a value of the type T. In the following code, three buttons in the DOM are first located and then turned into three event streams:

//
val theUp_btn = $extval(ptr, "$(\"#theUp_btn\")")
val theDown_btn = $extval(ptr, "$(\"#theDown_btn\")")
val theReset_btn = $extval(ptr, "$(\"#theReset_btn\")")
//
val theUp_clicks =
  $extmcall(EStream(ptr), theUp_btn, "asEventStream", "click")
val theDown_clicks =
  $extmcall(EStream(ptr), theDown_btn, "asEventStream", "click")
val theReset_clicks =
  $extmcall(EStream(ptr), theReset_btn, "asEventStream", "click")
//
Note that the type EStream(ptr) is assigned to the generated event streams as the items in these streams are not intended to be analyzed. A stream theComb_clicks of the type EStream(act) is created as follows by merging three streams:
//
datatype act = Up | Down | Reset
//
val theUp_clicks = theUp_clicks.map(TYPE{act})(lam _ => Up())
val theDown_clicks = theDown_clicks.map(TYPE{act})(lam _ => Down())
val theReset_clicks = theReset_clicks.map(TYPE{act})(lam _ => Reset())
//
val theComb_clicks = merge(theUp_clicks, theDown_clicks, theReset_clicks)
//
where the datatype act is declared for differentiating the items in theComb_clicks. The map function on an event stream applies its second argument, a closure-function, to each item in its first argument, which is an event stream. In this regard, it is similar to the map function on a lazy-stream.

A property theCounts is created by scanning the stream theComb_clicks:

//
val
theCounts =
scan{int}{act}
(
  theComb_clicks
, 0 // initial count
, lam(res, act) =>
  (
    case+ act of
    | Up() => min(res+1, 99)
    | Down() => max(0, res-1)
    | Reset() => 0 // the default
  )
) (* end of [theCounts] *)
//
Given a type T, a property of the type Property(T) in Bacon.js essentially refers to a value of the type T paired with a stream of the type EStream(T). The property theCounts is assigned the type Propert(int) and its initial value is 0. Whenever an item enters the stream theComb_clicks, the third argument of scan, which is a binary closure-function, is called on the current value associated with the property theCounts and the item to produce the next value associated with the property.

Finally, the following call to onValue is made on theCounts with a closure-function:

//
val () =
theCounts.onValue()
(
lam(count) =>
{
  val d0 = count%10 and d1 = count/10
  val d0 = String(d0) and d1 = String(d1)
  val theCount2_p = $extfcall(ptr, "jQuery", "#theCount2_p")
  val ( (*void*) ) = $extmcall(void, theCount2_p, "text", String(d1)+String(d0))
}
) (* end of [val] *)
//
The closure-function is first called on the initial value associated with the property theCounts. Whenever the next value on the property theCounts is available, a call to this closure-function is trigged on the value.

The following demo is of a counter that is implemented in the FRP-style outlined above:


00



For the entirety of this implementation, please see the source code on-line.

A Simple Auto-Counter of FRP-style

The following demo shows an auto-counter that can change its count automatically:


00



If the Auto button is set, then pressing the Up/Down button results in the displayed count being increased/decreased by 1 every second until another button is pressed. If the Auto button is not set, then pressing the Up/Down button results in the displayed count being increased/decreased by 1 only once.

The following code constructs a stream theAuto_clicks of the type EStream(act) and a property theAuto_toggles of the type Property(bool):

//
datatype act = Up | Down | Reset | Skip
//
val theAuto_btn = $extval(ptr, "$(\"#theAuto3_btn\")")
val theAuto_clicks =
    $extmcall(EStream(ptr), theAuto_btn, "asEventStream", "click")
val theAuto_clicks = theAuto_clicks.map(TYPE{act})(lam _ => Skip())
val theAuto_toggles = scan{bool}{act}(theAuto_clicks, false, lam(res, _) => ~res)
//
Note that Skip is added to the datatype act, which indicates no change to the count maintained by the auto-counter. The property theAuto_toggles is a boolean one indicating whether the Auto button is set.

The function Property_sampledBy_estream_cfun in the following code constructs a stream by essentially interpreting each item in the stream theComb_clicks based on the current value of theAuto_toggles:

//
val theAutoComb_stream =
  Property_sampledBy_estream_cfun
    (theAuto_toggles, theComb_clicks, lam(x, y) => if x then Skip else y)
//
An item entering theComb_clicks is interpreted as Skip in theAutoComb_stream if the Auto button is set. Otherwise, it is interpreted as what it is.

In the following code, the call to the function Bacon_interval generates a stream of zeros such that a zero enters the stream every 1000 milliseconds (that is, 1 second), and the function Property_sampledBy_estream takes a property and a stream and returns another stream consisting of all the values in the given property sampled at each point when an item enters the given stream:

//
val theTick_stream =
  Property_sampledBy_estream
    (theAuto_toggles, Bacon_interval{int}(1000(*ms*), 0))
//
Therefore, theTick_stream is a stream of booleans obtained by sampling theAuto_toggles at the interval of every 1000 milliseconds.

The following code constructs a stream theComb2Tick_stream by interpreting each tick entering theTick_stream based on the current value of the property theComb2_property:

//
val theComb2_clicks = merge(theComb_clicks, theAuto_clicks)
val theComb2_property = EStream_toProperty_init(theComb2_clicks, Skip)
//
val theComb2Tick_stream =
  Property_sampledBy_estream_cfun
    (theComb2_property, theTick_stream, lam(x, y) => if y then x else Skip)
//
The tick is interpreted as Up, Down, or Reset if the last pressed button is Up, Down, or Reset, respectively. Otherwise, the tick is interpreted as Skip.

Finally, a property theCounts can be constructed as follows by scanning the stream obtained from merging theAutoComb_stream and theComb2Tick_stream:

//
val
theCounts =
scan{int}{act}
(
  merge
  (
    theAutoComb_stream
  , theComb2Tick_stream
  )
, 0 (*initial*)
, lam(res, act) =>
  (
    case+ act of
    | Up() => min(res+1, 99)
    | Down() => max(0, res-1)
    | Reset() => (0) | Skip() => res 
  )
) (* end of [theCounts] *)
//
The code for displaying the values in the property theCounts is omitted. For the entirety of this implementation of an auto-counter, please see the source code on-line.

Compiling and Testing

Please find in the following files the entirety of the code presented in this article:

JS/theCounter_callback.dats // counter of callback-style
JS/theCounter2_baconjs.dats // counter of FRP-style via Bacon.js
JS/theCounter3_baconjs.dats // auto-counter of FRP-style via Bacon.js
In addition, there is an accompanying Makefile for compiling and testing the code.


This article is written by Hongwei Xi.