This is a quick start tutorial which aims to explain the basics of Turbine as briefly as possible.
Turbine is a fairly small library build on top of the FRP library Hareactive. When learning Turbine the bulk of the work is actually to learn Hareactive and FRP. This tutorial covers most of Turbine but only the essential types in Hareactive and a small fraction of its API.
The following code is a small complete Turbine app:
import Hareactive.Combinators (accum)
import Turbine (Component, use, component, output, (</>), runComponent)
import Turbine.HTML as E
counter :: Component { count :: Behavior Int } {}
counter id = component \on -> do
count <- accum (+) 0 (on.increment <> on.decrement)
( E.div { class: pure "wrapper" } (
E.text "Counter " </>
E.span {} (E.textB $ map show count) </>
E.button {} (E.text "+" ) `use` (\o -> { increment: o.click $> 1 }) </>
E.button {} (E.text "-" ) `use` (\o -> { decrement: o.click $> -1 })
)
) `output` { count }
main = runComponent "#mount" counter
The code creates an application that functions as shown in the GIF below.
This small example uses almost every important function in Turbine. Hence, by explaining every detail of the example from top to bottom this tutorial covers most of Turbine.
Let's first consider each of the types used in the example. The three key FRP types from Hareactive all come into play:
- Behavior:
A
Behavior a
represents a value of typea
that changes over time. In the example abovecount
has the typeBehavior Int
because the count changes over time in response to the buttons. - Stream:
A
Stream a
represents events or occurrences that happens at specific moments in time. In the example,increment
anddecrement
are streams created from the click events on each button. - Now.
A
Now a
represents a computation that runs in an atomic moment of time, which has access to the current time, and which can have side-effects. ANow a
can be though of as equivalent toTime -> Effect a
. In the example the function passed tocomponent
returns aNow
hence thedo
makes the function run in theNow
-monad.
The primary type which Turbine adds on top of Hareactive is
Component.
A component is an encapsulated description of a piece of user interface as well
as the logic and state controlling it. The Component
type constructor takes
two type parameters. For instance, counter
in the example is of the type:
counter :: Component { count :: Behavior Int } {}
The first argument to Component
is called the component's available output
and the second is called the component's selected output. Similarly to how
Effect a
denotes a computation with side-effects that produces a value of
type a
the type Component a b
denotes a component that when constructed
produces two outputs, one of type a
and one of type b
. A component's
available output represents all the events and values that it exposes and the
selected output is the part of this which the user of the component has
explicitly declared interest in.
If you think of a DOM element, then the available output corresponds to all the
events that we could listen to by calling addEventListener
on the element
and the selected output corresponds to the things that we have already declared
interest in by calling addEventListener
on the element.
In the example counter
is a component created with the component
function.
A Turbine application is structured using components and the component
function is the primary way to create custom components. It has the type:
component :: forall a o p. (o -> Now (ComponentResult a o p)) -> Component p {}
Values of type ComponentResult
are constructed with the output
function:
output :: forall a o p. Component a o -> p -> Now (ComponentResult a o p)
Hence, the function given to component
essentially returns two values. A
component of type Component a o
and a value of some type p
. The function
then receives an argument of type o
. Notice the recursive dependency: the
selected output of the returned component is passed as input to the function.
The selected output of the component returned in the example has the type { increment :: Stream Int, decrement :: Stream Int }
. These streams come from
the HTML view construct further down and they correspond to the click events
from each of the buttons. The increment
stream has an occurrence with the
value 1
whenever the "+" button is pressed and the decrement
stream has an
occurrence with the value -1
whenever the "-" is pressed.
Streams are monoids and the expression on.increment <> on.decrement
results
in a new stream that combines the occurrences of both streams.
We then apply accum
to the combined streams: accum (+) 0 (on.increment <> on.decrement)
. The accum
function can be though of as a "fold over time" and
it has a type that resembles that of foldr
.
accum :: forall a b. (a -> b -> b) -> b -> Stream a -> Now (Behavior b)
accum
returns a value in the Now
monad because whenever we accumulate state
over time the result depends on when we start accumulating. If you count how
many cars pass by a certain road starting from right now you are not going to
have the same count as someone who started counting yesterday. Hence, accum
returns a Now
and the Now
returned to component
is run when the component
is constructed. Thus, our counter will begin accumulating right when it is
constructed. It starts with the initial value 0
(the second argument to
accum
) and every time the stream has an occurrence +
(the first argument to
accum
) is applied to the current value and the value of the occurrence. The
result becomes the new current value.
As in the example below component
is typically used following this pattern or
template:
myComponent = component \on -> do
// Logic/model
...
// View
view `output` { ... }
As indicated returned component is often called the componen's "view" and the code above is called the component's "logic" or "model". The view can use values defined in the model and the model can use selected output from the view. This circular dependency arises naturally when using FRP to build UI.
The next part of the example is the part that constructs the HTML for the counter:
E.div { class: pure "wrapper" } (
E.text "Counter " </>
E.span {} (E.textB $ map show count) </>
E.button {} (E.text "+" ) `use` (\o -> { increment: o.click $> 1 }) </>
E.button {} (E.text "-" ) `use` (\o -> { decrement: o.click $> -1 })
)
The module Turbine.HTML
contains element functions for creating components
that correspond to HTML elements. These takes a record of attributes and a
child component---except in cases when a HTML doesn't support one or both of
those things.
As mentioned, in FRP behaviors are used to represent values that change over
time. To support creating HTML that change over time behaviors can be given to
the element functions. Since we want the outer div
to constantly have the
class wrapper
we use pure "wrapper"
which creates a constant behavior.
The function textB
has the type Behavior String -> Component {} {}
. It
takes a behavior and returns a component which creates a text node that changes
its content as the given behavior changes. Therefore the expression E.textB count
creates a text node that always shows the current count.
Components are composed with the merge
function, used as its </>
operator.
The expression a </> b
is a new component which creates the HTML from a
followed by the HTML from b
. merge
has the type:
merge :: forall a o b p q. Union o p q => Component a { | o } -> Component b { | p } -> Component {} { | q }
From the constraint Union o p q
the PureScript compiler infers q
to be the
union of o
and p
. This means that when combining two components merge
throws away the two component's available output (notice how a
and b
does
not appear in the return type) and it combines the two component's selected
output as the selected output of the resulting component. Hence, when composing
components their selected output propagates out into the final component while
the available output it discarded.
If, for some component, we want to use parts of its available output we apply
the use
function. This function copies output from the available part into
the selected part. It has the type:
use :: forall a o p q. Union o p q => Component a { | o } -> (a -> { | p }) -> Component {} { | q }
In other words, use
takes a component and a function. The component's
available output is passed to the function which must return a record. A new
component is returned with its selected output being the union of the existing
selected output and what the function returned.
We are now equipped with the knowledge necessary to understand this part of the example:
E.button {} (E.text "+" ) `use` (\o -> { increment: o.click $> 1 }) </>
E.button {} (E.text "-" ) `use` (\o -> { decrement: o.click $> -1 })
The expression E.button {} (E.text "+")
has the type Compenent { click :: Stream ClickEvent, ... } {}
. In other words, a stream of click events is part
of the available output from a button. For each of the buttons we apply use
to select the click stream first with the property name increment
and then
with the property name decrement
. Streams are functors and we use $>
to
turn the value of each event or occurrence into 1 and -1 respectively. Finally,
we compose the two components with </>
and the resulting type of the above
becomes:
Component {} { increment :: Stream Int, decrement :: Stream Int }
This composed component is then given as the child component to the div
function:
div :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o
Notice how the div
function propagates the selected output of its child into
the selected output of the returned component. All the element functions behave
in this way and, combined with how </>
works, this ensures that selected
output always "bubbles up" when HTML is constructed. The final component passed
to output
therefore has the selected output { increment :: Stream Int, decrement :: Stream Int }
and this gets passed as input to the function given
to component
. We have come full circle.
The last line calls runComponent
which has the type:
runComponent :: forall a o. String -> Component a o -> Effect Unit
The function takes a CSS selector
string and a
component. It then runs the component with the first element matching the
selector as its parent. Similarly to how Effect
describes side-effects a
Component
only describes a piece of UI or a part of an application. A Turbine
application always has an invocation of runComponent
which then runs the
entire application.