This tutorial introduces and explains the core concepts in Turbine. Along the way we build a few simple applications to exemplify the material.
Turbine is based on functional reactive programming (FRP). It uses the FRP library Hareactive. It is important to understand that Turbine is a relatively small layer on top of Hareactive that provides abstractions for building HTML in a way that is connected with FRP. Hareactive is a substantially larger library than Turbine. Both in terms of implementation but, more importantly for the tutorial, also in terms of the size of the API and of how much there is to learn. Hence, when learning Turbine the bulk of the learning is actually to learn FRP. This tutorial assumes no prior experience with FRP and hence it can also be seen as a tutorial to FRP in general and Hareactive in particular.
If you want to you can follow along the tutorial yourself and potentially experiment with the code examples. You can do so by cloning the Turbine starter template with the following commands.
git clone https://github.com/funkia/purescript-turbine-starter turbine-tutorial
cd turbine-tutorial
npm i
After having executed the above you can run npm run build
and afterwards you
should see the text "Hello, world!" if you open the index.html
file in a
browser. Along the way you should make changes to the file src/Main.purs
,
rebuild the app with npm run build
, and then observe the changes in a
browser.
The central type in Turbine is Component
. As a first approximation a
Component
represents a piece of user interface. For instance, it could be
an input field or a button. More concretely, a Component
contains a
description of how to create a piece of HTML. Components are composable. Hence
an input field and a button can be composed together and the result is another
component. A component also describes any state, logic, and side-effects
associated with the component. As an example, two input fields and a button can
be composed to describe the UI of a login form. The logic for the login form
and the side-effects for performing the HTTP requests for the login can be
"attached" to the view.
A Turbine application is constructed by composing components. Components divide the app into separate chunks that can be implemented in isolation. A Turbine application is "components all the way down".
The Component
type has the following kind:
Component :: Type -> Type -> Type
That is, it is parameterized by two types. The purpose of these are explained later in the tutorial.
In this section we will explain how to create static HTML with Turbine. Turbine
contains functions for creating components that correspond to single HTML
elements. These live in the module Turbine.HTML
, which is typically imported
qualified like this:
import Turbine.HTML as H
For every HTML element the Turbine.HTML
module exports a corresponding
function. For the HTML element div
there is a div
funtion. For the span
element there is a span
function, and so on. The first argument to these
functions is a record of attributes. For HTML elements that can contain
children, the corresponding function takes a second argument, as well. This
argument must be a component and describes the child of the element. Here are
a few examples:
myInput = H.input { placeholder: "Write here", class: "form-input" }
myButton = H.button {} (H.text "Click me")
myDivWithButton = H.div { class: "div-class" } myButton
The H.text
function used above takes a string and returns a component
corresponding to a text node of the given string.
Components are composed together with the </>
operator. As a first
approximation, </>
is similar to the semigroup operator <>
, which has the
type a -> a -> a
. The type of </>
, however, is slightly more complex,
since components keep track of more information at the type level than a
typical semigroup. Writing component1 </> component2
creates a new component
which represents the HTML from the first component followed by the HTML for the
second component. As an example the code:
myLoginForm =
H.input { placeholder: "Username" } </>
H.input { placeholder: "Password" } </>
H.label {} (H.text "Remember login") </>
H.checkbox {}
Corresponds to the following HTML:
<input placeholder="Username" />
input placeholder="Password" />
<label>Remember login</label>
<input type="checkbox" />
If you add the code above to Main.purs
and change the definition of Main
into the following:
-app = H.text "Hello, world!"
+app = myLoginForm
Then you should see HTML corresponding to the HTML above.
By combining </>
with the fact that each element function accepts a child
component as its second argument, we can create arbitrary HTML of any
complexity. In this tutorial we will build a simple counter application
(similar to the one shown above). To this end let us create
the HTML which we will use going forward.
counterView =
H.div {} (
H.text "Counter " </>
H.span {} (H.text "0") </>
H.button {} (H.text "+") </>
H.button {} (H.text "-")
)
Here we have hard coded the value 0
into the user interface. The intended
outcome is that the displayed number is dynamic. It should increment every time the
+
button is pressed and decrement every time the -
button is pressed. But,
in order to achieve that we need to learn a little bit of FRP.
At its essence functional reactive programming can be seen as providing
abstractions for representing phenomena that depend on time in a purely
functional way. FRP contains two key data-types Behavior
and Stream
:
- A
Behavior
represents a value that changes over time. - A
Stream
represents events or occurrences that take place at discrete moments in time.
For instance, Behavior Number
represents a changing number and Behavior String
represents a changing string. On the other hand, a Stream Number
represents numbers associated with discrete moments in time, and Stream String
represents strings associated with discrete moments in time.
Note: What we call
Stream
is often calledEvent
in other FRP libraries.
The difference between behaviors and streams can be illustrated as below:
As the image indicates, a behavior can be seen as a function from time. That is, at any specific moment in time it has a value. A stream on the other hand only has values, or occurrences, at specific punctuations in time.
Initially, the distinction between a behavior and a stream may be unclear. Fortunately, when one becomes familiar with the two abstractions, the choice of which one to use becomes unambiguous. A simple heuristic to determine whether a particular thing should be represented as a behavior or stream is to ask the question, "does this thing have a notion of a current value?". If "yes", it is a behavior. If "no", it is a stream. Turbine uses behaviors and streams to represent any dynamic UI value using FRP. Here are a few examples:
- The value of an input field is represented as a
Behavior String
. Because the input field always has a "current value", its value is represented as a behavior. - The clicking of a button is represented as a
Stream ClickEvent
. A click of the button is an event that happens at discrete moment in time, hence a stream is used. - Whether or not a checkbox is checked is represented as a
Behavior Boolean
.
In the counter component above, we hard coded the value 0
into the view. The
goal is to have the displayed number change over time. And, as mentioned,
in FRP we use behaviors to represent values that changes over time. Thus, we
parameterize the HTML above such that it takes as argument a record of a
behavior of the type Behavior Number
:
counterView { count :: Behavior String } -> Component _ _
counterView { count } =
H.div {} (
H.text "Counter " </>
H.span {} (H.textB (map show count)) </>
H.button {} (H.text "+") </>
H.button {} (H.text "-")
)
We also changed H.text "0"
into H.textB (map show count)
. The textB
function is similar to text
except that, instead of taking an argument of type
String
, it takes an argument of type Behavior String.
It then returns a
component that describes dynamic HTML. The value of the text node will be
kept up to date with the value of the behavior.
We have now modified the view such that it takes as input a dynamic count which it displays in the UI. Next we must declare the view's output such that the clicks of the two buttons.
Recall that the Component
type is parameterized by two types. Both of these
are, by convention, almost always records. The first of them is called the
component's selected output and the second is called the component's available
output. If you are familiar with addEventListener
in the DOM API then, as an
analogy, the available output can be thought of the events that we could
listen to by calling addEventListener
with the event name. The selected
output, on the other hand, is the output that we have explicitly declared that
we are interested in.
When a component is initially created its selected output is usually {}
. This
matches the intuition that a newly constructed component has not had any of its
available output selected yet. The available output on the other hand will be a
record of all the various streams, behaviors, and other things that the
component produces.
As an example, consider this slightly simplified type of the button
function:
button :: { | a } -> Component {} { click :: Stream ClickEvent
dbclick :: Stream ClickEvent
-- ... and so on
}
This type tells us that when given a record of attributes the button
function
returns a component with available output as declared by the last object. It
includes, among other things, a field of type click :: Stream ClickEvent
.
This stream has an occurrence whenever the button is pressed.
As another example, consider the type of the input
function:
input :: { | a } -> Component {} { value :: Behavior String
, blur :: Stream FocusEvent
, keydown :: Stream KeyboardEvent
, keyup :: Stream KeyboardEvent
-- ... and so on
}
From this we see that a component constructed by the input
function has among
its available output a field of type value :: Behavior String
. This behavior
describes the current value of the input field.
Available output can be selected by using the
output
function. Its type is as follows:
output :: forall a o p q. Union o p q => Component { | o } a -> (a -> { | p }) -> Component { | q } a
Let us unpack the type piece by piece. The output
function takes as
arguments a component and a function. The type variable a
is the component's
available output. The function takes the available output, the a
, and returns
a record of p
. The given component's selected output is the type variable o
.
Per the constraint Union o p q
the type variable q
becomes the union of o
and p
. The returned component has the type Component { | q } a
. In other
words, the given function receives the component's available output, returns a
record, and this record is then merged into the returned component's selected
output. The end result is that output
moves output from the available part
into the selected part.
The output
function is often used infix as in the following example.
usernameField = H.input {} `output` (\o -> { username: o.value })
In the above code we are selecting the value
behavior that the component
created by input
outputs. By returning a record with a field named username
we are in a sense moving the behavior from the available output into the
selected output and renaming it at the same time. As defined above
usernameField
has the type Component { username :: Behavior String } { ... }
.
As the piece in the puzzle to understand how output works we must now
consider the type of the </>
operator which is an alias for the
merge
function.
merge :: forall a o b p q. Union o p q => Component { | o } a -> Component { | p } b -> Component { | q } { | q }
Due to the Union o p q
constraint merge
takes two components and returns a
new component that is their combination. This combination has as its selected
output the union of the two components' selected output.
Let us return to the example with the login form from earlier. Consider how we might get output from the view and how the types interact:
myLoginForm =
H.input { placeholder: "Username" } `output` (\o -> { username: o.value }) </>
H.input { placeholder: "Password" } `output` (\o -> { password: o.value }) </>
H.label {} (H.text "Remember login") </>
H.checkbox {} `output` (\o -> { rememberLogin: o.checked })
Each invocation of output
selects some output and each invocation of </>
merges these in the combined components. The end result is that myLoginForm
has the type:
myLoginForm :: Component { username :: Behavior String
, password :: Behavior String
, rememberLogin :: Stream Boolean
}
{ ... }
The
modelView
function is a key part of Turbine. It is the primary way to create custom
components with custom logic. It takes a model and a view. The model is a
function that returns a computation in the
Now
monad. The view is a function that returns a component:
modelView :: forall o p a x. (o -> x -> Now p) -> (p -> x -> Component o a) -> (x -> Component { } p)