What is this tutorial about?
In this tutorial you are going to learn how to create a flashcard app. A flashcard is a
card bearing information on both sides, which is intended to be used as an aid in memorization. Each flashcard bears a question on one side and an answer on the other. Flashcards are often used to memorize vocabulary, historical dates, formulae or any subject matter that can be learned via a question-and-answer format.
Stack
The stack we are going to use in this tutorial is
The reason for this eclectic mix is because it was the preference of @keinegurke_. They wanted to learn both Elm, Rust and German. But an in depth tutorial was missing. Since flashcards could help them improve their vocabulary, they soon settle on building an app and documenting their progress in this tutorial.
Prerequisites
What would you need to start with this tutorial? First of all; in interest in either Elm, Rust, or both. The focus of this tutorial is in learning these technologies. Second is a computer and some experience with running commands in the command line. Modern development relies increasingly on tools that require some proficiency with the command line.
If you qualify, dive right in!
Installing Elm
Since Elm is one of the technologies we are using to create our flashcard app we better start with installing it.
Installation
How Elm can be installed is described in their installation page. This includes setting up elm in a number of editors. Which certainly will come in handy.
What is missing do are some helpful tools. Most notably elm-test
and elm-format
.
elm-test
is used to test your Elm code, a practice that provide even more
confidence in refactoring your code.
elm-format
automatically formats your code, so that no wars can erupt over
where to place which comma.
You can learn how to install elm-test
from their README.
Similarly, how to install elm-format
is described in
their README as well.
Verification
If everything worked out as planned you should be able to execute the following commands
elm --version
This should respond with the latest version. At the time of writing that was
0.19.0
.
Similarly, the following commands
elm-test --version
responds at the time of writing with 0.19.0-rev6
.
Finally, verifying that elm-format
is a little different. The command does not
have a --version
flag. Instead asking for --help
elm-format --help
Should declare it's version at the top of its help page. Again. At the time of
this writing that was 0.8.1
.
Celebrate
You are now all set for the next chapter. But before you go and explore the wonderful world of Elm, make sure you celebrate! Configuring your machine can be very hard.
Skeletal Elm project
With all the Elm
tools installed. It is time to take them for a spin. Right
from the Elm is a very friendly language. When you ask Elm for help
elm --help
It will thank you for trying out Elm, give you some pointers where to find
more information and tell you about some command options. We are
interested in the elm init
command. It promises to
Start an Elm project. It creates a starter elm.json file and provides a link explaining what to do from there.
This is a nice starting point. Run the command to start an Elm project. The skeletal project creates the following files and directories.
.
├── elm.json
└── src
The elm.json
describes the Elm project. We describe it in more detail later in
the tutorial. Besides the elm.json
file there is a directory called src
.
This is where all the source files go.
An empty src
directory is an unhappy src
directory. So let's go ahead and
create a file named FlashCard.elm
inside the src
directory with the
following content
module FlashCard exposing (..)
import Html
main =
Html.text "Hello, World!"
Verification
One of the cool things about Elm is that it can serve your files. By spinning up a reactor Elm will look at your files, create a web server, serve your files and when necessary compile them on the fly.
elm reactor
The above command will start the Elm reactor. The message that it returns with
asks you to got to http://localhost:8000. Going to that
address you will find a file navigation pane. Navigate it to src
>
FlashCard.elm
and be greeting by your very own Hello, World!
A first flash card
Let's focus on how the structure of the Html should look like. In order to use
the capabilities of Elm way of doing Html we imported the Html
module.
import Html
A module
let you break your code into multiple files
You can define you own modules, but we will wait until the opportunity arises. First we will be using modules defined in packages as found on Elm packages
Html
module
The Html
module has documentation as found in the Elm packages. It needs a
little getting used to, but eventually you will get the hang of it.
If we would write plain Html that would display a flash card, we could write something like this
<div>
<span>Wettervorhersage</span>
</div>
Note that wettervorhersage is a German word meaning weather forecast. Maybe the snippet of Html is missing some attributes and classes, but for now we will focus on the structure.
To mimick the snippet in Elm, we would write the following code in elm
main =
Html.div []
[ Html.span [] [ Html.text "Wettervorhersage" ]
]
There are several things going on here. Not that Elm is a functional language. This means that functions are the building blocks of creating programs.
There are three functions used to recreate the Html. Html.div
, Html.span
and
Html.text
. Almost all functions in the Html module that produce tags accept
two arguments. A list of attributes, and a list of content.
We haven't defined attributes yet so the first argument of Html.div
and
Html.span
is the empty list: []
. The second argument of Html.div
is a
list with content, i.e. a span that itself contains text.
We will have more to say about functions but for now we can verify that everything works as expected.
Verification
When you reload the flash card app in the browser, you should see the German word for weather forecast.
A custom function
We have created our first flash card, but it can only show "Wettervorhersage". Although it is a great word, we would like to be able to show different words. Let's make that possible with the introduction of a custom function.
What we would like to achieve is to write
main =
view "Wettervorhersage"
and let the view
function return the Html for the flash card. For this we need
to create a function. Functions are defined by stating their name, i.e. view
,
and their arguments. For us that would be the text that we want to show, so
let's call the argument for our function text
.
Next would comes the body of the function and it defined the return value. For
us that looks very similar to our first flash card. The only difference is that
instead of the hard coded "Wettervorhersage"
, we want to show the argument,
I.e. text
.
Putting all this together, the view
function comes down together
view text =
Html.div []
[ Html.span [] [ Html.text text ]
]
Verification
If your reload the application you should still see our flash card that displays "Wettervorhersage". Changing the argument of the view function and reloading should reflect in the app.
Type annotations
We have just created a custom function. It is lacking one of the Elm strongest features. I.e. type information.
Types help a compiler to determine if programs are correct. They can be used to verify that the expectations you have about functions and programs are met, even before the program is run.
Up until now, Elm has inferred the types of our functions. But relying on the
type inference could hide some problems. So let's annotate our view
function
with it's type.
Common types
Elm has some built in types that you can use right out of the gate. One that we
already have used is String
. It is a chunk of text. Elm also
provides a myriad of functions for String
but now we are only interested in
it's type.
Remember that Elm is a functional language? Because functions are
so important you can provide type information for them as well. This is done
with an arrow ->
.
On the left side of the arrow you place the type of the argument of the
function. To the right of the arrow you place the type of the return value. But
what is that? Let's guess it to by String
and see what Elm makes of it.
Providing type information for the view
function is done by
view : String -> String
This is placed right above the definition of the view
function.
Verification
If you reload the application you will be greated by a friendly message from the Elm compiler
-- TYPE MISMATCH --------------------------------------------- src/FlashCard.elm
Something is off with the body of the
view
definition:11|> Html.div [] 12|> [ Html.span [] [ Html.text text ] 13|> ]
This
div
call produces:Html.Html msg
But the type annotation on
view
says it should be:String
We guessed the return type for the view
function to be String
. But that is not
correct. Elm noticed that the type difference from what actually is returned.
Let's change the type signature of the view
function to
view : String -> Html.Html ()
for now. We will go into the convoluted Html.Html ()
later.
Once you reload the application again. You should be greeted by "Wettervorhersage".
Namespaces
We have just specified the return type of the view
function to be
Html.Html ()
. Why are there two mentions of Html
? We will answer that
question in this chapter.
Look at the top of the file. There is an import line.
import Html
This imports the Html
module from the Htmll
package. As can be
seen from the documentation, this package provides a lot of useful functions.
In order to use these functions you have to use there fully qualified name. E.g.
if you want to use the function div
that will render a div element on the
page. You write
Html.div
The reason for using the fully qualified name is to avoid name clashes. Let's illustrate a name clash with an example.
Favorite number
We are going to ask somebody to create a module providing us with their
favorite number. Kirsty told us that her favorite number is 42
. Daan favorite
on the other hand is 37. We could create modules that record these favorites.
We will settle on the name number
to record the respective favorite number. If
we wanted to use both numbers in one program, we would not have a problem. We
can use fully qualify the name and we would still know which number we mean.
If we wanted to use Kirsty's number we would write Kirsty.number
and get 42.
If instead we wanted to use Daan's number, we would wirte Daan.number
.
Second Html
We learned about the first Html
in the signature of the view
function. It is
the Html namespace. A namespace will prevent name clashes between different
modules. But what about the second Html
.
If you take a look at the Html
documentation you will find it
exposes a type alias Html
. The Html
type can be used to annotate
functions like our view
function that it is returning Html.
Exposing names
Sometimes there is no danger of name clashes. E.g. it will be unlikely that we
will using two Html
types. In order to shorten the Html.Html
part to just
the type Html
we can expose the Html
type into our own namespace.
We can expose names from modules when we import them. In order to expose the
Html
type from the Html
module we would write
import Html exposing (Html)
This allows us to write the signature of the view function ascii
view : String -> Html ()
If you find it tedious to have to fully qualify the div
and span
functions
from the Html
module. the import line would read
import Html exposing (Html, div, span)
Verification
In this chapter you learned about namespaces, fully qualified names and exposing names in your own namespace. In order to verify if you have grasped these concept, try to answer the following question.
Why can't we expose the text
function from the Html
module?
A record type
We annotated our view
function with it's type, but something does not feel
right. Shouldn't a flash card have a front and a back? Just a single String
does not cut it.
Let's remedy this with a record.
Records
A record
can hold many values, and each value is associated with a name.
Our flash card has a front and a back, so describe the flash card type with record seems a nice fit.
Instead of accepting a String
as the argument to our view
function, we
should accept a record that has a front
and a back
.
view : { front : String, back : String } -> Html ()
Now the argument name of text
isn't very well chosen anymore. Lets change it
to card
.
view card =
...
Since we renamed the argument from text
to card
, we can't use text
in the
body of the function anymore. Let's replace it with the front of the card
div []
[ span [] [ Html.text card.front ]
]
We now introduced a new type; a record with a front
and a back
, both of type
String
. But we haven't changed to argument we are calling the view
function
with.
main =
view { front = "Wettervorhersage", back = "Weather forecast" }
Verification
If you reload the web app once more. You should still see "Wettervorhersage".
Type alias
Our flash card type now is { front : String, back : String }
which, to say the
least, does not roll off the tongue easily. Instead we would like to name our
flash card type FlashCard
.
This can be achieved with a type alias. A type alias will introduce a name for a different type. This has several benefits. Among them it allows you to have a recognizable name for a type.
If you want to introduce a type alias you use the type alias
keywords. In our
case you could use it like
type alias FlashCard =
{ front : String, back : String }
With the type alias introduced we can clean up the signature of the view
function by changing it to
view : FlashCard -> Html ()
Verification
Fundamentally, nothing changed to the program, we only introduced a name for a familiar thing. The application should still show "Wettervorhersage" when reloaded.
A custom type
We have created a fuller flash card. It has a front and a back. But what way is it facing? Are we seeing the front or the back?
If we want to differentiate we should be able to express the difference. We will do that with a custom type.
Face
The following code introduces a new type Face
. It will be used to determine
which way our flashcard is facing.
type Face
= Front
| Back
The type
construct is an example of an algebraic data type. That is a
fancy name saying that there are certain ways of combining different types into
more complex types. We will come back to that subject later on.
Extending FlashCard
With a Face
we should extend our FlashCard
model. In the FlashCard
type
alias add a field face
of type Face
.
type alias FlashCard =
{ front : String
, back : String
, face : Face
}
This will break our program. This is because when we created a flash card we should also specify which way it is facing.
{ front = "Wettervorhersage", back = "Weather forecast", face = Front }
Verification
If it compiles, it should still show you "Wettervorhersage" when you reload the app.
A convenient function
If we want to create a flash card we have to write
{ front = "Wettervorhersage", back = "Weather forecast", face = Front }
That is a whooping 71 characters. Most of these characters are boilerplate. Can't we do better than that?
Functions to the rescue
Of course we can do better. We can create a function that helps us with creating a flash card.
Let's think about a name for the function. It creates a flash card so
flash_card
seems appropriate.
Having settled on the name let's think about the signature. We would create
flash cards facing Front
. So the only parts that are moving are the front and
the back.
This means our functions needs to accept two String
s and returns a
FlashCard
. The way the describe that in Elm is
flash_card : String -> String -> FlashCard
It needs some getting used to, but there are good reasons why the signature looks the way it does.
With the signature in place, we can go on to defining the function.
flash_card front back =
{ front = front, back = back, face = Front }
Verification
We can now use the flash_card
function. We do need some parenthesis, or else
Elm will get confused.
`main =
view (flash_card "Wettervorhersage" "Weather forecast")
Other than that, everything should still work. As an other verification: what if
we wanted to pass which way the flash card is facing to our flash_card
function, how would the signature change?
Showing the back
We have been staring at Wettervorhersage for a while now. We would like to
convince ourselves that we still know it's translation. Time to leverage the
face
of our FlashCard
and show a different side.
Let expression
In our view function we hard-coded to show the front of the flash card.
view card =
div []
[ span [] [ Html.text card.front ]
]
We like to be able to change which text we show depending on the face that is
showing. For this we are going to introduce a let
-expression in our
view
function.
A let
expression is useful when
when an expression is getting large. You can make a let to break it into smaller definitions and put them all together in a smaller expression.
We first use it to introduce a name for the text we want to show.
view card =
let
text = card.front
div []
[ span [] [ Html.text text ]
]
In it self this isn't a big change. It only allows us to define a name. But the value we bind this name is now under our control.
Showing the back
We would like to show the a different text when we face the back.We can achieve that by matching on the flash cards face.
text =
case card.face of
Front ->
card.front
Back ->
card.back
this makes use of new concept: Pattern matching. A case
expression
allows us to branch based on which variant we happen to see.
So if our card faces Front
we want our card to show the card.front
. And when
it faces Back
we want to see card.back
.
Verification
Make a prediction what would happen when you reload the web application. If your prediction matches the reality you probably know what is up next.
Flip the card
When we reloaded the application we still saw Wettervorhersage. The reason for this is that the model we are viewing hasn't changed. In order to see something different we need to flip the card.
A flip
function
Let's create a function that will do the flipping for us. We start by thinking
about the signature, i.e. what goes in the function and what should come out.
Since we want to flip a FlashCard
picking that as a argument to our function
makes sense. The result should be a FlashCard
again. So our flip
function
has a signature of
flip: FlashCard -> FlashCard
When we start thinking of the implementation we quickly run into the problem of
determining the opposite of a face. I.e. when we are facing Front
the opposite
should be Back
and vice versa.
Let's cater to that now and so that we can use that function in the definition
of flip
.
An opposite
function
Again, we start thinking of the signature of our function. In should go a Face
and out should come a Face
again.
opposite: Face -> Face
We just learned about the let
-expression, which comes in handy now. Because it
allows us to determine which value occurs so we can take action accordingly.
opposite : Face -> Face
opposite face =
case face of
Front ->
Back
Back ->
Front
flip
reloaded
With a new brush in our art kit we are able to continue our implementation of
flip
. We should return a FlashCard
with the same front and back as our
input, but with the opposite face.
flip card =
{ front = card.front, back = card.back , face = opposite card.face }
Pfew, that was a lot of typing. Isn't there a better way? I certainly would not want to type all those fields if our model grows.
flip
revolutions
Luckily Elm allows that. It basically allows us to say; take this flashcard and only modify the fields we are interested in. The syntax is shown below and is exactly similar to the code above.
flip card =
{ card | face = opposite card.face }
Sweet!
Verification
We have a flip
function, but we need to use it.
main =
view (flip (flash_card "Wettervorhersage" "Weather forecast"))
Reloading the web application now will show "Weather forecast", but at a cost of a lot of parenthesis. Elm has a way to form a pipeline. Below you can see how it works.
main =
flash_card "Wettervorhersage" "Weather forecast"
|> flip
|> view
Here we first create a flash card with the flash_card
function. We pass that
flash card to the flip
function via the |>
operator. The result of the flip
function is in its turn passed to the view
function via the same |>
operator.
We will take a closer look at the |>
operator and other functions.
Elm Architecture
We are capable to flip a card, but we want to control when we flip it. At the moment we are only generating static content. How does Elm allow values to change.
In this chapter we will look into the Elm architecture.
Model-View-Update
The Elm architecture is a pattern that help in structuring an interactive application.
It revolves around a Model, a View and a way to Update the model. Two of the
parts are in place already. Our model is a FlashCard
with view
as it's View
function.
What is missing is the Update function and how it interacts with Elm.
Pure functions
Functions in Elm are pure. A pure function is
a function that has the following properties:
- Its return value is the same for the same arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams from I/O devices).
- Its evaluation has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or I/O streams).
This property has some nice benefits. But how do we change things, if functions should always return the same on the same input?
Elm runtime
The Elm runtime is the escape hatch that allows change in our web app. The runtime is responsible for the Model. It will use a View function to show our application. It will Update the model in the way we tell it to.
The way we tell it to is by sending a Message. This Message allows the Update function to differentiate how to update the model.
We will be sending Message from the application, e.g. by clicking a button.
This probably all sounds very abstract. It will become clear in the next chapter. Note that every Elm application is structured, even recursively, with the Elm architecture. This helps developers in managing their code base.
Updating the model
Let's work on applying the Elm architecture and update the model.
Message
We first start to think about the message our model understands. At the moment
we are only interested in flipping a card. A good message would be Flip
. Let's
start from there
type Message
= Flip
Now we created the ability to express the fact that we want to change our model.
Model
We keep talking about our model. We know that it is a flash card, but that could change in the future. Aligning our code with the Elm way has some benefits. So let's introduce an alias.
type alias Model =
FlashCard
This way we can talk about Model
in our signatures and it will align with Elm,
but we haven't lost the ability to address is as a FlashCard
.
Update
Let's create an update
function. The signature of the update model is
determined by Elm. It should benefits
update : Message -> Model -> Model
We give it a Message
and a Model
. Update will return a Model
depending on
the two. The implementation is straight forward, since there is only one
instance of the Message
type.
update message model =
case message of
Flip ->
flip model
Match on the message, and for each case, even when there is only one, determine
what to do. The flip
function is what we need in the Flip
case.
Wire it all together
Now we need to tell Elm how these parts work together. There is a
Browser
module that offers different programs. We will be
interested in sandbox
.
In order to be able to use the Browser
module we need to import it.
import Browser
The sandbox needs a structure with the following fields
init
. The initial model.view
. The view function used to display the model.update
. an finally the update function used.
We all have those in place. So we can
Verification
Change the main
variable to
main =
let
model =
flash_card "Wettervorhersage" "Weather forecast"
in
Browser.sandbox
{ init = model
, view = view
, update = update
}
And now we can finally flip the flash card.
Building the frontend
A browser can not execute Elm. We first must make our Elm code into a form that the browser will understand.
Luckily the elm
command knows how to do that.
elm
make
There is a sub-command that is able to create files that the browser knows
about. It is called elm make
. Running elm make --help
show
The `make` command compiles Elm code into JS or HTML:
elm make <zero-or-more-elm-files>
For example:
elm make src/Main.elm
This tries to compile an Elm file named src/Main.elm, putting the resulting
JavaScript code in an elm.js file.
We left out the flags that can be passed to this sub command.
Let's use the sub command to make an index.html
. This way we don't need to
reactor to show our app.
elm make src/FlashCard.elm
The command above should create an index.html
in the current work directory.
Verification
Open the just generated index.html
with a browser, and check that
Wettervorhersage still means Weather forecast.
Installing Rust
We will use Rust to create a web server. Before we can start writing our web server, we should install Rust.
Installation
Rust can be installed and updated via a handy tool called rustup
as described
on the installation page. This allows
you to have multiple different versions of Rust live next to each other.
When you execute the installation command, the install script will asks some questions. Make sure you read and understand what the script will try to achieve. For this tutorial no customization is necessary.
When the scripts finished Rust is installed. This comes with a couple of tools.
Verification
To make sure that the installation was successful the following commands should run without error.
cargo --version
The above command should respond with the installed cargo version. At the time
of this writing that was 1.38.0
.
rustc --version
Should respond with the same version.
Creating a client directory
What we have been working on up until now is the client for our application. With Rust installed, we quickly will move into writing server code. We better setup our project source tree to reflect our intentions.
This entails little more than to creating a client
directory and moving our
stuff over there.
Verification
Make sure that running the Elm reactor, or building the frontend still works as expected.
Skeletal Rust project
We used our frontend tooling to create a skeletal project for us. Just like with our frontend, our backend tooling allows us to do the same.
cargo new
The cargo
command is the go to place for Rust development.
cargo --help
Shows all sort of sub-commands available. The one we are interested in is the
cargo new
command.
cargo new --help
shows use
Create a new cargo package at
It has some options that we are going to use. The following command
cargo new --bin --name 'flashcard-server' server
will produce the structure below.
server
├── Cargo.toml
└── src
└── main.rs
The Cargo.toml
files describes the project and there already is a main.rs
source file scaffold.
Verification
Go into the server
directory and run the
cargo run
command. This will compile your sources and greet you with a friendly Hello, world!
.
Simple Server
In this chapter we are going to create a simple server. Our goal is to make it greet us when we make a request.
Gotham
We will be leveraging the Rust eco-system instead of reinventing the wheel our self. There are a lot of packages available to us. They live at crates.io. One of the packages we will be using is Gotham.
Gotham is a
flexible web framework that promotes stability, safety, security and speed.
Add dependency
Before we can use a package we need to declare it as a dependency. This is done
in the Cargo.toml
. We need to add our dependency to the dependencies
table.
[dependencies]
gotham = "0.4.0"
Hello, world!
Next we will create a minimal server that responds with Hello, world!
with
every request.
Change main.rs
with
extern crate gotham; use gotham::state::State; const HELLO_WORLD: &'static str = "Hello World!"; pub fn say_hello(state: State) -> (State, &'static str) { (state, HELLO_WORLD) } pub fn main() { let addr = "localhost:8886"; println!("Listening for requests at http://{}", addr); gotham::start(addr, || Ok(say_hello)) }
This is taken from the hello-world example from the Gotham page.
Verification
Run the server with the command
cargo run
This will compile main.rs
and the transitive dependencies. This can take some
time. When it finishes it announces
Listening for requests at http://localhost:8886
Now open a browser and navigate to
http://localhost:8886
and you will be greeted with an
Hello, world!
.
Serving the frontend
Wouldn't it be interesting to serve the frontend from our server? Let's work on that in this chapter.
For this it is best to have an assets
directory that will hold all of our
static assets. In the server directory create an assets
directory and move the
index.html
to it.
assets/
└── index.html
Serving static files
Gotham allows us to define a router. A router allows you to control which handlers get to handle certain requests. In order to use it we need some imports.
# #![allow(unused_variables)] #fn main() { use gotham::router::builder::{build_simple_router, DefineSingleRoute, DrawRoutes}; #}
We don't need the HELLO_WORLD
, or say_hello
, so we can get rid of it.
Instead we will define the router to serve our index.html
. We use the
build_simple_router
factory for that.
# #![allow(unused_variables)] #fn main() { let router = build_simple_router(|route|{ route.get("/").to_file("assets/index.html"); }); #}
The build_simple_router
accepts a closure that enables us to configure the
routes. We want to respond with index.html
when we make a get request.
Server with routes
We now need to start the server with our router.
# #![allow(unused_variables)] #fn main() { gotham::start(addr, router); #}
Verification
Restart the server by shutting it down and running cargo run
again. This will
recompile the server and start waiting for requests. Open your browser and open
the application. It should serve the frontend application.
Decoding a FlashCard
We would like the let the server decide which flash card to show. In order for
that to work, we need to be able to receive JSON data and decode that into a
FlashCard
.
This chapter we will be working on that.
json package
There is a json
package that will help us decode, and encode, JSON
into datatypes that we are interested in. Before we can use it we need to
install it.
elm install elm/json
Json.Decode
We will be using the Json.Decode
module from the json
package. Because we
want to avoid typing Json.Decode
everywhere when we want to refer to a type
from that module, we are importing it with a different name.
Since we will be defining a Decoder
we will expose that type into our
name space.
import Json.Decode as Decode exposing (Decoder)
Decoder
We want a Decoder
for FlashCard
. The type therefore should be
decode : Decoder FlashCard
Decoders work by composing primitive decoders into more elaborate once. Primitive decoders include
Decode.string
that will decode a string value.Decode.int
that will decode an int value.Decode.bool
that will, well you can probably guess what type of value it decodes.
More elaborate decoders are for example Decode.map
or variants. It's signature
is
Decode.map : (one -> other) -> Decoder one -> Decoder other
It takes a function that maps one type into the other, a decoder for one and will produce a decoder for other. Under the cover the one decoder is used and when successful the transformation is applied to the result.
JSON
Below we will give an example of the JSON we expect to receive. It probably does not surprise you that it looks like
{ "front": "Wettervorhersage", "back": "Weather forecast", "face": "Front" }
Decoder FlashCard
We will create a decoder that will ignore the face field of the JSON. When we have a little more experience we will come back and decode that as well. That means that the decoder becomes
decode : Decoder FlashCard
decode =
Decode.map3 FlashCard
(Decode.field "front" Decode.string)
(Decode.field "back" Decode.string)
(Decode.field "face" <| Decode.succeed Front)
We use Decode.map3
and the FlashCard
constructor, which got automatically
created when we introduced the type alias, to map three different values into a
FlashCard
.
We extensively use the Decode.field
decoder. It
decodes a JSON object, requiring a particular field. The object can have other fields. Lots of them! The only thing this decoder cares about is if a field is present and that the value there is an of a certain type.
Lastly we use Decode.succeed
to hard code the face value to be Front.
Verification
We have created a decoder but how do we know that it behaves correctly? We can write a test to see if our decoder can decode some JSON.
In tests/FlashCardTest.elm
write the following code after our other test.
, test "decode a flash card from JSON" <|
\_ ->
let
input = """{ "front": "Wettervorhersage", "back": "Weather forecast", "face": "Back" }"""
expected =
flash_card "Wettervorhersage" "Weather forecast"
|> Ok
actual =
decodeString FlashCard.decode input
in
Expect.equal actual expected
and all the tests should still work when you import
import Json.Decode exposing (decodeString)
Testing Elm
We would like to send data from the server to the client, interpret the data as a flash card. There are a lot of things that can go wrong. In order to gain some confidence that some things are going right, we can write automated tests.
This chapter will setup everything that is needed to start writing tests for our Elm code.
Using elm-test
We have installed elm-test
but we haven't used it yet. Now is a good time. Go
over to the client
directory and run the following command
elm-test init
This will ask to update elm.json
with packages that enable us to writing
tests. Agree with the proposed plan. When the command finished there is a new
directory tests
.
tests/
└── Example.elm
We can rename Example.elm
to FlashCardTest.elm
run the test suite
Running the test suite amounts to executing the command
elm-test
It responds with the following overview
TEST RUN INCOMPLETE because there is 1 TODO remaining
Duration: 253 ms
Passed: 0
Failed: 0
Todo: 1
↓ FlashCardTest
◦ TODO: Implement our first test. See https://package.elm-lang.org/packages/elm-explorations/test/latest for how to do this!
The reason it reports that there is a TODO is because the generated test file provided it. Let's change that
Write a Test
Replace the todo
with the following code
describe "FlashCard"
[ test "updating a flash card with a `Flip` message flips the flash card" <|
\_ ->
let
a_flash_card =
flash_card "Wettervorhersage" "Weather forecast"
expected =
flip a_flash_card
actual =
update Flip a_flash_card
in
Expect.equal actual expected
]
Let's start understanding this from the top. First is the describe
function.
It accepts a String
that should tell you what is under test and a list of other
tests.
Next we see a test
function. It also accepts a String
that should tell you
what we are testing. Furthermore, test
accepts as it's second argument a
function that can do the actual testing.
In our case the function is a closure, i.e. an anonymous function. In the
function we use a let
-expression to set up some variable bindings which we use
to express our expectations. This is achieved with the Expect.equal
function.
For this to work we need to import our precious FlashCard
functions and types.
We can do this with the following line
import FlashCard exposing (..)
Verification
We have learned a lot about how a test is written, but does it work? Run the
elm-test
command and it should report
TEST RUN PASSED
Duration: 214 ms
Passed: 1
Failed: 0
Model on server
If we want the server to send us a flash card, the server first have to have a notion of what to send.
In this chapter we our going to model flash cards on the server.
lib.rs
The skeletal project generated a main.rs
. It would be possible to write all
the necessary code in this file, but that would quickly become unmaintainable.
Rust recognizes this fact and allows for code to write in a library instead.
Create a file lib.rs
in the src
directory with the following content
# #![allow(unused_variables)] #fn main() { mod domain; #}
The above definition defines a module called domain
. Let's go ahead and create it.
domain/mod.rs
There are various ways to define a module. One way is to create a directory in
the src
directory with the same name as the module, in our case domain
, and
a file mod.rs
in that directory.
struct
We would like to focus on modeling flash cards on the server. We have some experience on the client side. Luckily, that experience translates very well to the server side.
Rust also knows about records. Rust calls them structs. Take a look at the following code
# #![allow(unused_variables)] #fn main() { pub struct FlashCard { front: String, back: String, face: Face, } #}
You probably recognizes a lot from Elm's FlashCard
. There are two noteworthy
things two point out.
First the pub
keyword. This allows other modules to use this struct. The other
is the Face
type. Rust doesn't know about it. So we better define it right
away.
enum
Our modeling experience on the Elm side helps us on the Rust side as well.
Again, there is a naming difference, but take a look at Face
# #![allow(unused_variables)] #fn main() { pub enum Face { Front, Back, } #}
Constructor
When we created our model in Elm, we quickly realized that it becomes tedious to spell out the structures every time we needed it. To mitigate that we created a constructor. Let's do the same in Rust.
It looks a bit different, but is similar enough that it is recognizable.
# #![allow(unused_variables)] #fn main() { impl FlashCard { pub fn new<S>(front: S, back: S) -> Self where S: Into<String> { Self { front : front.into(), back : back.into(), face : Face::Front } } } #}
The essence is the following line
# #![allow(unused_variables)] #fn main() { Self { front : front.into(), back : back.into(), face : Face::Front } #}
It looks a bit strange with the front.into()
and back.into()
but that is
because Rust knows different kinds of strings. The input type S : Into<String>
allows us to conveniently add any type that knows how to transform
itself into a String
.
Verification
We haven't changed our server and only added code. It should still be able to build our code.
# #![allow(unused_variables)] #fn main() { cargo build #}
The command above should do just that. Other than given some warnings about our new code not being used, it should compile our code.
Serialize FlashCard
We have modeled flash cards on the server. We now need to turn it into JSON before we can send it to the client.
In this chapter we will use serde to automatically serialize our flash cards.
Dependency
We will need two dependencies that we need to list in our Cargo.toml
. Together
they allow us to serialize flash cards into JSON. Add serde
and serde_json
to the dependencies table in Cargo.toml
.
serde = { version = "1.0.101", features = [ "derive" ] }
sserde_json = "1.0.41"
Use serde
In order to leverage the serialization capabilities of serde_json
we need to
create a serde
serializer. There is a very cool trick that serde
knows that
makes this a breeze.
The first thing we need to do is to announce we our needing serde
.
# #![allow(unused_variables)] #fn main() { use serde::Serialize; #}
The other thing is that we need to tell serde
that we would like it to
derive serializers for our data types. We can do this by annotating both
FlashCard
and Face
with a derive
annotation. E.g. Face
would be
annotated like
# #![allow(unused_variables)] #fn main() { #[derive(Serialize)] pub enum Face { Front, Back, } #}
Verification
With the automatic part of serde done, it is good to see if serialization works
like we expect. The best way to do this is to test is. Rust integrates testing
right at the place where the source is created. Add the following code at the
end of src/domain/mod.rs
.
# #![allow(unused_variables)] #fn main() { #[cfg(test)] mod test { use super::*; use serde_json; #[test] fn flash_card_can_be_serialized_to_json() { let flash_card = FlashCard::new("Wettervorhersage", "Weather forecast"); let actual = serde_json::to_string(&flash_card).unwrap(); let expected = r#"{"front":"Wettervorhersage","back":"Weather forecast","face":"Front"}"#.to_owned(); assert_eq!(actual, expected); } } #}
There is some boilerplate to let the test framework know about what we want to test. The actual test is
# #![allow(unused_variables)] #fn main() { let flash_card = FlashCard::new("Wettervorhersage", "Weather forecast"); let actual = serde_json::to_string(&flash_card).unwrap(); let expected = r#"{"front":"Wettervorhersage","back":"Weather forecast","face":"Front"}"#.to_owned(); assert_eq!(actual, expected); #}
It creates a FlashCard
, serializes it to JSON with serde_json
, sets up an
expectation, and checks it the expectation is met.
If you run
cargo test
it should give the all green.
Sending FlashCard
We are capable to serialize a flash card to JSON. The next step is to send it to the frontend. We will do this by making an end point where the frontend can request a flash card.
This probably will be a somewhat longer chapter because there are a lot of moving pieces. So make sure to fetch yourself a nice drink and strap in for the ride.
Dependencies
We will be using some dependencies that need to be declared in the Cargo.toml
.
Specifically the following dependencies
hyper = "0.12.35"
mime = "0.3.14"
Create a new route
The next step is to dream up a new route. Where should the frontend get a
flash card from. We are offering an API and it is reasonable to adhere to the
REST model. One suggestion is to choose /api/flashcard
.
In main.rs
we have configured a router. We can extend it with a route for a
flash card
# #![allow(unused_variables)] #fn main() { route.get("/api/flashcard").to(get_flash_card); #}
the get_flash_card
handler is something that does not yet exist. But it is
something we like. In order to use it we should do
- Announce the use of an external crate. In
main.rs
at the top add
# #![allow(unused_variables)] #fn main() { extern crate flashcard_server; #}
- Use the
get_flash_card
function. Also inmain.rs
add an otheruse
expression
# #![allow(unused_variables)] #fn main() { use flashcard_server::handler::flashcard::*; #}
Create a new module
From the usage we learn that there should be a handler
. So we need to announce
that in out lib.rs
# #![allow(unused_variables)] #fn main() { pub mod handler; #}
We will create this module by using a sub-directory handler
that contains a
mod.rs
file. We probably are going to create a lot of handlers for this
applications so the contains of the handler/mod.rs
could look like this.
# #![allow(unused_variables)] #fn main() { pub mod flashcard; #}
This just announces an other module. This time do, we are not creating a
sub-directory. Instead we are creating a file named flashcard.rs
in the
handler
directory. This is the other way of creating a module.
flashcard.rs
should have our definition of get_flash_card
.
get_flash_card
A handler has a particular signature for the gotham web framework to work with
it. In goes a State
, a provided by gotham. At goes a tuple of a state, and
whatever you want to return. All in all this amounts to the following code.
# #![allow(unused_variables)] #fn main() { use gotham::state::State; use crate::domain::FlashCard; pub fn get_flash_card(state: State) -> (State, FlashCard) { let flash_card = FlashCard::new("Kodieren", "Encode"); (state, flash_card) } #}
Here we create a new flash card and remove it with the state unaltered.
Response
The web is build about requests and responses. Each response can be of a certain MIME type, but it can't know everything that could be send back. So we will need to tell gotham how to create a real response from a flash card.
There is a trait that you can implement that does precisely that. It
is called IntoResponse
and it will allow us to specifiy how to turn a flash
card into a response.
In order to use it we need to declare it, and related type.
# #![allow(unused_variables)] #fn main() { use gotham::handler::IntoResponse; use gotham::helpers::http::response::create_response; use gotham::state::State; use hyper::{Body, Response, StatusCode}; #}
Next we should implement the IntoResponse
trait.
# #![allow(unused_variables)] #fn main() { impl IntoResponse for FlashCard { fn into_response(self, state: &State) -> Response<Body> { create_response( state, StatusCode::OK, mime::APPLICATION_JSON, serde_json::to_string(&self).expect("serialized flash card"), ) } } #}
Here we use the create_response
function to create a valid response, which
contains JSON with a body of a string representation of the flash card.
Verification
When you restart the application and open a terminal you should be able to execute the following curl command
curl -X GET http://localhost:8886/api/flashcard
and the server should respond with
{"front":"Kodieren","back":"Encode","face":"Front"}⏎
Moving out of the sandbox
Up until now we have been relying on Elm's sandbox. The sandbox allows for a user to interact with the component in a limited way. For example, it is not possible to make an HTTP call.
We would like to do just that, so it is time to move out of the sandbox.
Browser.element
The next step up on the Elm ladder is Browser.element
. From the
element documentation we learn
Unlike a sandbox, an element can talk to the outside world in a couple ways
We will give a brief description of those ways, paying attention which are relevant for us.
Commands
Commands are a way to request the Elm runtime to do a task. Making an HTTP call is an example.
Commands are a way to describe a certain action. This keeps your program pure, i.e. function always return the same value when called with similar arguments. The Elm runtime will respond by performing the command you specified.
The Elm runtime notifies you of the result by calling the update function.
Subscriptions
Sometimes you are interested in something your component is not responsible for. E.g. ticking of time, or mouse clicks. If you are interested in these things you can subscribe to these events.
Again, Elm runtime notifies you of an event from a subscription by calling the update function.
Flags
Let's we are creating our app and our customers want there name embossed on the flash card. The functionality would not change, besides that our model would keep track of the customer name.
One way to configure our application is by sending flags. This allows an application to change behavior right from the start. E.g. one could pass in via a flag the address one should retrieve flash cards from.
Ports
If you want even more communications with the outside world, ports allow you to send and receive messages. They are the defacto way of communicating with anything outside the control of the Elm runtime.
For example if you want to make use of a JavaScript library that does not have an native Elm counterpart one should use ports to establish a bridge
Updating update
The update
function needs to be updated because it needs to be able to fire of
commands. This dual responsibility is reflected in the signature. For
completeness we show the current signature.
update : Message -> Model -> Model
Besides updating the model, We could potentially also create a command, to be executed by the Elm runtime. The new signature is
update : Message -> Model -> ( Model, Cmd Message )
This will break the implementation. For now it is enough to create a tuple with
as second component Cmd.none
.
Flip ->
( Maybe.map flip model, Cmd.none )
Default subscriptions
At the moment we don't want to subscribe to anything. We can tell Elm that by
configuring Sub.none
, similar to Cmd.none
.
In order to prepare for future extension we will be creating a subscriptions
definition that we will reference when we are creating a Browser.element
.
subscriptions : Model -> Sub Message
subscriptions _ =
Sub.none
Updating init
When we will introduce a Browser.element
our current init
field from the
Browser.sandbox
does not suffice anymore. Again. We could kick off our entire
process by doing a request. Furthermore, our initial model could depend on some
flags.
For now we want to ignore the flags, and don't want to send a command. It suffices to change our init to.
init = \_ -> ( model, Cmd.none )
Replace Browser.sandbox
with Browser.element
Now we can replace Browser.sandbox
with Browser.element
. It will still
complain, because it is missing a subscriptions
field. But we prepared for
that.
All in all our program definition looks like
Browser.element
{ init = \_ -> ( Nothing, cmd )
, view = view
, update = update
, subscriptions = subscriptions
}
Verification
Building the app and opening the application would still allow you to learn the
translation of Wettervorhersage
.
Prepare to receiving a FlashCard
We have transformed our sandbox into an actual element. This allows us to make HTTP calls. We could put that to good use because our server is ready to send us a flash card, we only need to ask.
In this chapter we will make a request for a flash card to our server, receive it and display it in our client.
Changing the model
Currently we assume we always have a flash card. Put since we are planning to receive one over the internet, things could go wrong. In order to be honest about this fact, we are going to change our model.
Currently we our model is an alias for FlashCard
type alias Model =
Maybe FlashCard
Let's modify it to reflect that we maybe don't have a flash card all the time.
type alias Model =
Maybe FlashCard
This breaks a few things that we need to fix.
view
The view
accepts a model so we need to adopt it to our new definition.
Earlier we could just take the flash card and display a meaningful representation. Now we first must make sure that we actual have a flash card and show a helpful message when we don't.
We can achieve this with a pattern match. Maybe
has two constructors, in our
case Just card
, where card
is a FlashCard
or Nothing
. So our view
functions needs to start with a pattern match like so
view : Model -> Html.Html Message
view model =
case model of
Just card ->
-- details below
Nothing ->
-- details below
The Just card
branch should do what we always did, changed to reflect the use
of the card
variable.
let
text =
case card.face of
Front ->
card.front
Back ->
card.back
in
div []
[ span [] [ Html.text text ]
, button [ Event.onClick Flip ] [ Html.text "flip" ]
]
When we don't have a flash card, i.e. in the Nothing
branch we need to report
this fact to the user.
div [] [ span [] [ Html.text "No flash card to show" ] ]
This wraps up our view
function.
update
Our update
function also accepts a model, so it needs to change as well.
Before we could just call the flip
function on our model when we received a
Flip
message.
Now, because it is wrapped in a Maybe
type, this will not work. The types
don't work out.
Luckily the Maybe
type has a convenient function called map
.
The signature of map
is
(a -> b) -> Maybe a -> Maybe b
which allows use to
Transform a Maybe value with a given function
I.e. allow us to apply a function to Maybe FlashCard
with out unpacking it
first. Our update function can therefore be
update : Message -> Model -> ( Model, Cmd Message )
update message model =
case message of
Flip ->
( Maybe.map flip model, Cmd.none )
Initial model
In our main
function we our creating a model by hand. At the moment it is just
a flash card, but it needs to account for the fact that maybe it isn't present.
The most dramatic change would be to pick Nothing
as our starting value.
Browser.element
{ init = \_ -> ( Nothing, Cmd.none )
, view = view
, update = update
, subscriptions = subscriptions
}
Verification
When you build the client and serve it from the server, this time you should see a message that there isn't a flash card.
Let's remedy that in the next chapter.
Receiving FlashCard
All the pieces are aligned for receiving a flash card from the server. Let's dive right into it.
New message
The Elm runtime needs a way to tell us that there is a new flash card from the
server. The Elm runtime wants to do that by sending the update
function a
message. Let's create a message for that specific event.
We want to be notified from a result from the server. Since we will be receiving
it over the network, a lot of things can go wrong. To acknowledge this we will
be making use of an other Elm type: Result
A Result is the result of a computation that may fail. This is a great way to manage errors in Elm.
Since the errors that occur will be coming from the network, we will be needing types from the standard library.
import Http
Let's use it in our Message
. We will be creating a message that the Elm
runtime can use to inform us.
type Message
= Flip
| Received (Result Http.Error FlashCard)
The new addition is Received
. It has a parameter of type
Result Http.Error Flashcard
. This tells us that we will receive the result of
a "computation" that when successful will return a FlashCard
.
Creating a command
At the moment we initialize our main definition with Nothing
as a model. We
would like to receive a flash card from the server. So we will command the Elm
runtime to fetch it for use.
We do need to tell the Elm runtime how to do that. We can do that by returning
an actual command instead of Cmd.none
. Let's focus on the let-block in the
main
function.
let
cmd =
Http.get
{ url = "/api/flashcard"
, expect = Http.expectJson Received decode
}
in
Here we create a command cmd
by calling Http.get
. It needs a structure with
an url
field. You might recognize the URL from when we routed it in the
server.
The structure also needs an expect field which needs to be a Http.Expect a
.
One way of creating such an Expect
is with the Http.expectJson
function.
In it's turn the expectJson
function needs two arguments. The first argument
is a way to turn a Result Http.Error a
into a Message
. Remember that we
created a Recieve
message constructor. This constructor is precisely the
function we need.
The second argument is telling the Elm runtime how to transform the JSON from
the server into a type we are interested in. Remember that we created a
Decoder FlashCard
for that. It was bound to decode
.
This wraps up the creation of the command. Let's focus on sending it.
Sending a command
Sending the command is a bit anti-climactic. It is nothing more than replacing
the Cmd.none
in the init
field of the structure fed to the Browser.element
function with our cmd
.
init = \_ -> ( Nothing, cmd )
Verification
This is the moment of truth. Building the client and restarting the server
should, when you open the application, show a flash card. It isn't the by now
familiar Wettervorhersage
one.
Instead it should show you the flash card defined on the server. It is placed there to remind you of a job well done.
Docker
We are able to ask for a flash card from the server and show it in the client. That is amazing! If you haven't already celebrated that, go ahead and do that now.
One of the improvements still ahead of us is being able to show many flashcards. At the moment the flash card shown is hard coded on the server. We would rather be able to retrieve it from a database.
We will be installing docker. Docker allows us to run applications, such as a database, inside of a container. This elevates us from keeping our machine up to date with the latest database software. Instead, we can just run a different container.
Install
Tot install docker you can visit their installation guide. It provides many option that are to numerous to reproduce here. So go ahead and follow the instructions that fits your specific situation.
Verification
You can verify if the installation was successful by running the following command
docker --version
It should respond with the version of docker that you installed. For me it responds with
Docker version 18.09.7, build 2d0083d
With docker installed, we are all set to starting containers.