I gave a talk at ElixirConf 2015 on combining the Phoenix web framework with the Elm programming language. This is the tutorial that was referred to in that talk.
The tutorial walks through the creation of a very basic seat saving application, like one you'd use when booking a flight for example. The application will do just enough to demonstrate the mechanisms for getting the two technologies talking to each other.
There is an accompanying repo for this tutorial. Each of the numbered steps has an associated commit so that you can just look at the diffs if you'd rather not read through the whole thing.
Introducing Effects
Currently our application only allows us to model a given state and perform actions that result in changes to that state. We create an initial state for our application with the init
function and thereafter are only able to change that state via the update
function. The update
function always returns a new Model and so the only way to do anything other than generate a new Model is to have some kind of side effect happening before we return the new Model. This is bad form in purely functional languages like Elm.
So, what if we wanted to perform some action that didn't directly affect the state of the application? Say, for example, we wanted to perform an HTTP request (an HTTP response may change the state of the application, but the initial HTTP request will not). Elm's StartApp (as opposed to StartApp.Simple) provides Effects for this purpose. Effects enable us to perform tasks such as HTTP requests and channel the results back through the application in a form that Elm understands.
Let's upgrade our application from StartApp.Simple to StartApp.
We'll start from the top. Change the
main
function to the following:app = StartApp.start { init = init , update = update , view = view , inputs = [] } main : Signal Html main = app.html port tasks : Signal (Task Never ()) port tasks = app.tasks
We start by defining a function
app
(we'll ignore its type annotation for now), and inside it we call thestart
function ofStartApp
, rather than ofStartApp.Simple
. This function takes a record with four keys:init
which takes the initial model provided by ourinit
function, theupdate
andview
functions as before, and a list ofinputs
. Inputs allow us to specify external Signals that provide Actions to our application. We'll revisit these later. For now we initialize with an empty list.We then change our
main
function so that it calls thehtml
function on the function returned by ourapp
function. This gives us access to the HTML that results from the View function we passed to StartApp.Likewise we create a port so that we can use any tasks that pass through StartApp. We'll discuss tasks and ports more in later posts.
In order to be able to use the new StartApp and related packages we need to change our existing
StartApp.Simple
import to the following:import StartApp import Effects exposing (Effects, Never) import Task exposing (Task)
And we'll need to import the Effects package.
cd web/elm elm package install evancz/elm-effects -y cd ../..
Now that we're using the new StartApp our initializer needs to return more than just the initial Model. It needs to return a tuple with the initial Model and an
Effects Action
. AnEffects Action
can be thought of as a way to send an Effect that will result in an Action. As we have no Action to send at this point we use a null Effect (supplied byEffects.none
).Change the
init
function to the following:init : (Model, Effects Action) init = let seats = [ { seatNo = 1, occupied = False } , { seatNo = 2, occupied = False } , { seatNo = 3, occupied = False } , { seatNo = 4, occupied = False } , { seatNo = 5, occupied = False } , { seatNo = 6, occupied = False } , { seatNo = 7, occupied = False } , { seatNo = 8, occupied = False } , { seatNo = 9, occupied = False } , { seatNo = 10, occupied = False } , { seatNo = 11, occupied = False } , { seatNo = 12, occupied = False } ] in (seats, Effects.none)
The
in
block now returns(seats, Effects.none)
.Because our
update
function steps the Model from one state to the next, it too needs to return this tuple of Model and Effects Action.update : Action -> Model -> (Model, Effects Action) update action model = case action of Toggle seatToToggle -> let updateSeat seatFromModel = if seatFromModel.seatNo == seatToToggle.seatNo then { seatFromModel | occupied = not seatFromModel.occupied } else seatFromModel in (List.map updateSeat model, Effects.none)
Now we have the option of either changing the state of the Model, or performing an Effect like an HTTP request, or both (or neither in the case of a NoOp).
If we visit http://localhost:4000 in our browser our application should look and behave the same as before.
Summary
We have now set up our Elm application to use Effects. This allows for Actions that do things other than change the Model. In part 9 we'll use Effects to get data for our Elm application from our Phoenix application over HTTP.