module Froc:Functional reactive programmingsig
..end
Froc
implements functional reactive programming in the style of
FrTime / Flapjax (but typed of course). It uses the dynamic
dependency graph of Acar et al. (self-adjusting
computation). Behaviors are presented as monadic values, using
ideas from Lwt
.
A behavior is a value that can change over time, but is defined
at all times. An event is defined only at particular instants
in time, with a possibly different value at each instant; when an
event takes value v
we say it occurs with value v
or fires v
. Values are sent to an event using the associated event sender. A behavior or an event is a signal when we don't
care to specify which one it is, and we say a signal "changes" to
mean that it changes if it is a behavior or occurs if it is an
event.
Most of the functions in Froc
create a new signal from one or
more existing signals. The output signals are dependents of the
input signals and the inputs are the dependencies of the
outputs. When a signal changes, its dependents are updated
(according to some update function specific to the signal). The
dependencies of a signal are updated before the signal is updated,
so that the update function sees a consistent view of the
dependencies. Signals may have listeners which are notified
when the signal changes. Listeners are like dependents but don't
compute new signals; they are just functions, called for their
effect.
When an event is sent (with send
), an update cycle begins,
during which the transitive dependents of the initial event are
updated, and their listeners notified. It is not allowed to call
send
again until the update cycle is finished, but
send_deferred
may be called, which queues the event. Queued
events are processed (each in a new update cycle) following each
update cycle until the queue is empty. Events occurring in the same
update cycle are simultaneous.
The body of a listener or update function (such as the function
passed to bind
) delimits a dynamic scope, which is governed by the signals to which it is attached. When a signal
changes, listeners and dependents attached within the dynamic
scopes it governs are detached, and any attached cleanup
functions are called. This cleanup allows unneeded values to be
garbage collected, and also prevents them from being recomputed
unnecessarily or erroneously. Dynamic scopes are nested; cleaning
up an enclosing scope cleans up its enclosed scopes. Attachments
created outside any dynamic scope can only be detached by calling
init
.
Ordinarily, the entirety of a dynamic scope is cleaned up when its
governing signal changes. A dynamic scope may be partially cleaned
up using a memo function (see memo
). It is possible (though
not encouraged) to hold a reference to a signal (by storing it in a
ref
, for instance) after it has been detached from its
dependencies; a detached signal is not updated when its
dependencies change.
Most functions returning behaviors take an optional eq
argument,
which gives an equality function on the value of the resulting
behavior. A behavior's dependents are only updated when the
behavior is updated with a value which is not equal (according to
the equality function) to the old value. The default equality holds
if the values compare
to 0
(incomparable values are always not
equal). It is encouraged that behaviors of the same type always be
given the same equality.
type
'a
result =
| |
Value of |
| |
Fail of |
'a
or exception.type
cancel
type +'a
behavior
'a
.val return : 'a -> 'a behavior
return v
is a constant behavior with value v
.val fail : exn -> 'a behavior
fail e
is a constant behavior that fails with the exception e
.val bind : ?eq:('a -> 'a -> bool) ->
'b behavior -> ('b -> 'a behavior) -> 'a behavior
bind b f
behaves as f
applied to the value of b
. If b
fails, bind b f
also fails, with the same exception. The update
function f
delimits a dynamic scope governed by b
.val (>>=) : 'a behavior -> ('a -> 'b behavior) -> 'b behavior
b >>= f
is an alternative notation for bind b f
.val blift : ?eq:('a -> 'a -> bool) -> 'b behavior -> ('b -> 'a) -> 'a behavior
blift b ?eq f
is equivalent to bind b (fun v -> return ?eq (f v))
,
but is slightly more efficient.val lift : ?eq:('a -> 'a -> bool) -> ('b -> 'a) -> 'b behavior -> 'a behavior
lift ?eq f b
is equivalent to blift b ?eq f
; it can be
partially applied to lift a function to the monad without yet
binding it to a behavior.val sample : 'a behavior -> 'a
sample b
returns the current value of b
, or raises b
's
exception if it is failed.val sample_result : 'a behavior -> 'a result
sample
but returns a result.val catch : ?eq:('a -> 'a -> bool) ->
(unit -> 'a behavior) -> (exn -> 'a behavior) -> 'a behavior
catch bf f
behaves the same as bf()
if bf()
succeeds. If
bf()
fails with some exception e
, catch bf f
behaves as f
e
. The function f
delimits a dynamic scope governed by bf()
.val catch_lift : ?eq:('a -> 'a -> bool) ->
(unit -> 'a behavior) -> (exn -> 'a) -> 'a behavior
catch_lift bf ?eq f
is equivalent to catch bf (fun e -> return
?eq (f e))
, but is slightly more efficient.val try_bind : ?eq:('a -> 'a -> bool) ->
(unit -> 'b behavior) ->
('b -> 'a behavior) -> (exn -> 'a behavior) -> 'a behavior
try_bind bf f g
behaves as bind (bf()) f
if bf()
succeeds. If bf()
fails with exception e
, try_bind b f g
behaves as g e
. The functions f
and g
each delimit a
dynamic scope governed by bf()
.val try_bind_lift : ?eq:('a -> 'a -> bool) ->
(unit -> 'b behavior) -> ('b -> 'a) -> (exn -> 'a) -> 'a behavior
try_bind_lift bf ?eq f g
is equivalent to try_bind bf (fun v ->
return ?eq (f v)) (fun e -> return ?eq (g e))
, but is slightly
more efficient.val join_b : ?eq:('a -> 'a -> bool) -> 'a behavior behavior -> 'a behavior
join_b b
behaves as whichever behavior is currently the value of b
.val fix_b : ?eq:('a -> 'a -> bool) ->
('a behavior -> 'a behavior) -> 'a behavior
fix_b bf
returns bf b'
where b'
behaves like bf b'
, but
delayed one update cycle.val notify_b : ?now:bool -> 'a behavior -> ('a -> unit) -> unit
notify_b b f
adds f
as a listener for b
, which is called
whenever b
changes. When b
fails the listener is not
called. The notification is cancelled when the enclosing dynamic
scope is cleaned up.
The listener is called immediately with the current value of the
behavior, unless now
is false. The function f
delimits a
dynamic scope governed by b
.
val notify_b_cancel : ?now:bool -> 'a behavior -> ('a -> unit) -> cancel
notify_b
, and returns a cancel handle (the notification
is still cancelled when the enclosing dynamic scope is cleaned up).val notify_result_b : ?now:bool -> 'a behavior -> ('a result -> unit) -> unit
notify_b
but the listener is called with a result when
the value changes or when the behavior fails.val notify_result_b_cancel : ?now:bool -> 'a behavior -> ('a result -> unit) -> cancel
notify_result_b
, and returns a cancel handle (the
notification is still cancelled when the enclosing dynamic scope
is cleaned up).val hash_behavior : 'a behavior -> int
Hashtbl.hash
is not appropriate
because behaviors contain mutable data.type +'a
event
'a
.type -'a
event_sender
'a
.val make_event : unit -> 'a event * 'a event_sender
'a
.val never : 'a event
val notify_e : 'a event -> ('a -> unit) -> unit
notify_e e f
adds f
as a listener for e
, which is called
with v
whenever e
occurs with value v
. When a failure
occurs the listener is not called. The notification is cancelled
when the enclosing dynamic scope is cleaned up.
The function f
delimits a dynamic scope governed by b
.
val notify_e_cancel : 'a event -> ('a -> unit) -> cancel
notify_e
, and returns a cancel handle (the notification
is still cancelled when the enclosing dynamic scope is cleaned up).val notify_result_e : 'a event -> ('a result -> unit) -> unit
notify_e
but the listener is called with a result when
a value or a failure is sent.val notify_result_e_cancel : 'a event -> ('a result -> unit) -> cancel
notify_result_e
, and returns a cancel handle (the
notification is still cancelled when the enclosing dynamic scope
is cleaned up).val send : 'a event_sender -> 'a -> unit
send s v
sends the value v
to the associated event e
, so
e
occurs with value v
.val send_exn : 'a event_sender -> exn -> unit
send_exn s x
sends the failure x
to the associated event e
,
so e
occurs with failure x
.val send_result : 'a event_sender -> 'a result -> unit
send_result s r
sends the result r
to the associated event
e
, so e
occurs with result r
.val send_deferred : 'a event_sender -> 'a -> unit
send_deferred s v
enqueues a call to send s v
for a future
update cycle.val send_exn_deferred : 'a event_sender -> exn -> unit
send_exn_deferred s x
enqueues a call to send_exn s x
for a
future update cycle.val send_result_deferred : 'a event_sender -> 'a result -> unit
send_result_deferred s r
enqueues a call to send_result s r
for a future update cycle.val next : 'a event -> 'a event
next e
passes on only the next occurence of e
; subsequent
occurrences are dropped.val merge : 'a event list -> 'a event
merge es
occurs whenever any of the events in es
occurs. If
more than one of the es
occurs simultaneously, the earliest one
in the list is passed on.val map : ('a -> 'b) -> 'a event -> 'b event
map f e
fires f v
whenever e
fires v
. The function f
delimits a dynamic scope governed by e
.val map2 : ('a -> 'b -> 'c) -> 'a event -> 'b event -> 'c event
map2 f e1 e2
fires f v1 v2
whenever e1
and e2
fire v1
and v2
simultaneously. The function f
delimits a dynamic
scope governed by e1
and e2
.val filter : ('a -> bool) -> 'a event -> 'a event
filter p e
is an event that fires v
whenever e
fires v
and p v
is true. The function p
delimits a dynamic scope
governed by e
.val collect : ('a -> 'b -> 'a) -> 'a -> 'b event -> 'a event
collect f b e
is an event that maintains an internal state s
(initialized to b
); whenever e
fires v
, s'
becomes f s
v
, the event fires s'
, and s'
becomes the new internal
state. The function f
delimits a dynamic scope governed by e
.
Special care must be taken when using collect
with behavior- or
event-valued events. The dynamic scope delimited by f
is
cleaned up on each occurrence of e
; any signals or created in
f
become detached on the next occurrence, so it is easy to wind
up with detached signals in s
.
This cleanup may be controlled through the use of memo
.
val join_e : 'a event event -> 'a event
join_e ee
occurs whenever the event which last occurred on ee
occurs.val fix_e : ('a event -> 'a event) -> 'a event
fix_e ef
returns ef e'
where e'
is an event that occurs
whenever ef e'
occurs, but in the next update cycle.val hash_event : 'a event -> int
Hashtbl.hash
is not appropriate
because events contain mutable data.val switch : ?eq:('a -> 'a -> bool) ->
'a behavior -> 'a behavior event -> 'a behavior
switch b e
behaves as b
until e
occurs, then behaves as the
last value of e
.val until : ?eq:('a -> 'a -> bool) ->
'a behavior -> 'a behavior event -> 'a behavior
until b e
behaves as b
until e
occurs with value b'
, then
behaves as b'
.val hold : ?eq:('a -> 'a -> bool) -> 'a -> 'a event -> 'a behavior
hold v e
takes on the last value which occurred on e
, or
v
if e
has not yet occurred (since hold
was called).val hold_result : ?eq:('a -> 'a -> bool) -> 'a result -> 'a event -> 'a behavior
hold
but initialized with a result.val changes : 'a behavior -> 'a event
changes b
occurs with the value of b
whenever b
changes.val when_true : bool behavior -> unit event
when_true b
fires whenever b
becomes true.val count : 'a event -> int behavior
count e
takes on the number of times e
has occurred (since
count
was called).val make_cell : 'a -> 'a behavior * ('a -> unit)
make_cell v
returns a behavior (with initial value v
) and a
setter function which changes the behavior's value. The setter
enqueues the update for a future update cycle, so it may be used
freely.val init : unit -> unit
val no_cancel : cancel
val cancel : cancel -> unit
val cleanup : (unit -> unit) -> unit
cleanup f
attaches f
to the enclosing dynamic scope, so it is
called when the scope is cleaned up. This is useful for cleaning
up external resources, such as GUI event handlers.val memo : ?size:int ->
?hash:('a -> int) -> ?eq:('a -> 'a -> bool) -> unit -> ('a -> 'b) -> 'a -> 'b
memo f
creates a memo function f'
from f
. When f' x
is
called from within an update function, there may be either a hit
or a miss. A hit happens when some f' x'
was called in the
previous run of the update function, when eq x x'
, and no later
call has already hit (that is, hits must happen in the same order
as the calls happened in the previous run). On a miss, f' x
calls f x
in a new dynamic scope, and stores its value for
possible reuse. On a hit, f' x
returns the value of the
previous call, and any updates necessary to make the value
consistent are executed; the dynamic scope of the previous call
is not cleaned up (so that the value remains attached to its
dependencies).
The main point of memo
is to avoid needless recomputation in
cases where a computation is governed by some signal but does not
actually use the signal's value. For example, in
let g = memo () fun x -> ... in
b >>= fun _ -> g 7
the returned behavior is indifferent to the value of
b
. Without memo
it would be recomputed every time b
changes; with memo
it is computed only the first time.
Because the dynamic scope of the previous call is not cleaned up
on a memo hit, memo
can be used purely to protect signals and
listeners from being detached when their governing signals
change. See the quickhull
example for an instance of this use.
The unit argument makes it possible to memoize a recursive function, using the following idiom:
let m = memo () in (* creates the memo table *)
let rec f x = ... m f y in
let f x = m f x
The default hash function is not appropriate for behaviors and
events (since they contain mutable data); hash_behavior
and
hash_event
should be used instead.
val set_exn_handler : (exn -> unit) -> unit
val set_debug : (string -> unit) -> unit
val bind2 : ?eq:('a -> 'a -> bool) ->
'b behavior ->
'c behavior -> ('b -> 'c -> 'a behavior) -> 'a behavior
val blift2 : ?eq:('a -> 'a -> bool) ->
'b behavior -> 'c behavior -> ('b -> 'c -> 'a) -> 'a behavior
val lift2 : ?eq:('a -> 'a -> bool) ->
('b -> 'c -> 'a) -> 'b behavior -> 'c behavior -> 'a behavior
val bind3 : ?eq:('a -> 'a -> bool) ->
'b behavior ->
'c behavior ->
'd behavior -> ('b -> 'c -> 'd -> 'a behavior) -> 'a behavior
val blift3 : ?eq:('a -> 'a -> bool) ->
'b behavior ->
'c behavior ->
'd behavior -> ('b -> 'c -> 'd -> 'a) -> 'a behavior
val lift3 : ?eq:('a -> 'a -> bool) ->
('b -> 'c -> 'd -> 'a) ->
'b behavior -> 'c behavior -> 'd behavior -> 'a behavior
val bind4 : ?eq:('a -> 'a -> bool) ->
'b behavior ->
'c behavior ->
'd behavior ->
'e behavior ->
('b -> 'c -> 'd -> 'e -> 'a behavior) -> 'a behavior
val blift4 : ?eq:('a -> 'a -> bool) ->
'b behavior ->
'c behavior ->
'd behavior ->
'e behavior -> ('b -> 'c -> 'd -> 'e -> 'a) -> 'a behavior
val lift4 : ?eq:('a -> 'a -> bool) ->
('b -> 'c -> 'd -> 'e -> 'a) ->
'b behavior ->
'c behavior -> 'd behavior -> 'e behavior -> 'a behavior
val bind5 : ?eq:('a -> 'a -> bool) ->
'b behavior ->
'c behavior ->
'd behavior ->
'e behavior ->
'f behavior ->
('b -> 'c -> 'd -> 'e -> 'f -> 'a behavior) -> 'a behavior
val blift5 : ?eq:('a -> 'a -> bool) ->
'b behavior ->
'c behavior ->
'd behavior ->
'e behavior ->
'f behavior -> ('b -> 'c -> 'd -> 'e -> 'f -> 'a) -> 'a behavior
val lift5 : ?eq:('a -> 'a -> bool) ->
('b -> 'c -> 'd -> 'e -> 'f -> 'a) ->
'b behavior ->
'c behavior ->
'd behavior -> 'e behavior -> 'f behavior -> 'a behavior
val bind6 : ?eq:('a -> 'a -> bool) ->
'b behavior ->
'c behavior ->
'd behavior ->
'e behavior ->
'f behavior ->
'g behavior ->
('b -> 'c -> 'd -> 'e -> 'f -> 'g -> 'a behavior) -> 'a behavior
val blift6 : ?eq:('a -> 'a -> bool) ->
'b behavior ->
'c behavior ->
'd behavior ->
'e behavior ->
'f behavior ->
'g behavior ->
('b -> 'c -> 'd -> 'e -> 'f -> 'g -> 'a) -> 'a behavior
val lift6 : ?eq:('a -> 'a -> bool) ->
('b -> 'c -> 'd -> 'e -> 'f -> 'g -> 'a) ->
'b behavior ->
'c behavior ->
'd behavior ->
'e behavior -> 'f behavior -> 'g behavior -> 'a behavior
val bind7 : ?eq:('a -> 'a -> bool) ->
'b behavior ->
'c behavior ->
'd behavior ->
'e behavior ->
'f behavior ->
'g behavior ->
'h behavior ->
('b -> 'c -> 'd -> 'e -> 'f -> 'g -> 'h -> 'a behavior) ->
'a behavior
val blift7 : ?eq:('a -> 'a -> bool) ->
'b behavior ->
'c behavior ->
'd behavior ->
'e behavior ->
'f behavior ->
'g behavior ->
'h behavior ->
('b -> 'c -> 'd -> 'e -> 'f -> 'g -> 'h -> 'a) -> 'a behavior
val lift7 : ?eq:('a -> 'a -> bool) ->
('b -> 'c -> 'd -> 'e -> 'f -> 'g -> 'h -> 'a) ->
'b behavior ->
'c behavior ->
'd behavior ->
'e behavior ->
'f behavior -> 'g behavior -> 'h behavior -> 'a behavior
val bindN : ?eq:('a -> 'a -> bool) ->
'b behavior list -> ('b list -> 'a behavior) -> 'a behavior
val bliftN : ?eq:('a -> 'a -> bool) ->
'b behavior list -> ('b list -> 'a) -> 'a behavior
val liftN : ?eq:('a -> 'a -> bool) ->
('b list -> 'a) -> 'b behavior list -> 'a behavior