Level 2 - Learning types
The goal of this level is to learn union types and type aliases, which we often use to represent state.
From here on we'll move in small steps, writing small chunks of code that will be a part of our final game, while using more and more features from functional programming and Elm along the way. Ready, set, go!
2.1 It's a new record!
We are going to create a representation of a "card" - something that is hiding a picture and can be flipped by the player. We'll start off by creating the equivalent data structure of a JavaScript object - a record. You can see the similarities between JavaScript objects and Elm records here:
// JavaScript object
var person = {
name: 'Tom Cruise',
expensiveShoes: true,
};
-- Elm record
person : { name: String, fancyShoes: Bool }
person =
{ name = "Tom Cruise"
, fancyShoes = True
}
Task: Create an Elm record with the type { id : String } called myCard. Use id = "1" for the initial value. This id string will refer to the file name of the image our card will be hiding.
2.2 Rendering HTML to the screen
All HTML tags have corresponding functions in Elm, and they all accept two parameters:
- a list of zero or more
Html.Attribute - a list of zero or more
Htmlnodes
<!-- HTML -->
<div class="ninja">
<span>Banzai!</span>
</div>
-- Elm
div [ class "ninja" ]
[ span [] [ text "Banzai!" ]
]
For example, the function to create a div node has this signature: div : List (Attribute msg) -> List (Html a) -> Html a
Note about
Html aDon't worry about that scary type
Html a- we'll learn more about that later! Simply put, it's just saying that "hey, our HTML will emit some actions later on, and they will be of typea(which is a type variable, or a wildcard).
Task: Write the function viewCard: { id: String } -> Html a, which should output the following HTML:
<div>
<img src="/cats/{card.id}.png" />
</div>
Hint
These functions will be useful (they are included in the standard library so you don't have to write them yourself):
div : List (Attribute msg) -> List (Html a) -> Html aimg : List (Attribute msg) -> List (Html a) -> Html asrc : String -> Attribute msg
To get the src function you should put import Html.Attributes exposing (..) near the beginning of your file.
Remember also that string concatenation is done with ++.
If you now substitute the greet call in main with viewCard called with the record you created earlier you should see a beautiful little kitten on you screen!
2.3 Union Types: Representing card state
Memory requires us to flip a card and reveal its image when clicked. This means we need a way to represent card state, as a card can be in one of three potential states: Open | Closed | Matched.
Think about how we'd store this state in JS. Most likely, we'd reach for a string:
{
id: '1',
state: 'open' // or 'closed' or 'matched'
}
This is obviously not very safe. This doesn't constrain us to using only the three possible values, and there's nothing to avoid typing errors. Elm and other ML-languages have a great feature for this use case: Union Types.
A union type is like a Java enumerable or C# enum - a union type is a value that may be one of a fixed set of values. Chess pieces, for example, can only be either white or black.
type PieceColor = White | Black
PieceColor is now a normal type in our system, just as String or Bool. White or Black are constructor functions. In this case they take zero arguments and return a value of type PieceColor. Or, expressed with a type signature:
White : PieceColor
Black : PieceColor
Union types may also carry data. This means that the constructor functions for such union type values aren't zero argument functions. Let's look at an example:
type CustomerAge = Unknown | Known Int
-- Unknown : CustomerAge
-- Known : Int -> CustomerAge
This can be used to represent a customer's age in a situation where we might not know the age.
We see that the constructor function Known takes an Int argument and returns a CustomerAge.
We can wrap any type of accompanying data within a union type value (like Known), and the type of the accompanying data doesn't have to be the same for all the value types within a union.
This is incredibly useful, and we will now make our own!
Task:
- Create a union type called
CardStatethat can be eitherOpen,ClosedorMatched(constructor functions are always capitalized). Enrich our previous
Cardrecord with a field calledstatethat carries aCardStatevalue. You will also have to update the signature ofviewCard.Our
myCardvalue should now have the following type signature:
myCard : { id : String, state : CardState }
2.5 Type Alias (alias slayer)
By now we see that our signature for card is getting unwieldy. Imagine maintaining the signatures for our card objects all around the codebase as we add more fields. It doesn't exactly scale.
Enter type aliases!
Type aliases allow us to...
- ...give a name to records with a specified structure, and use it as a type.
- ...define a record with a specified data structure as a new type.
Let's look at an example.
customer : { name : String, age: CustomerAge }
customer =
{ name = "Evan"
, age = Unknown
}
getName : { name : String, age: CustomerAge } -> String
getName customer =
customer.name
If we create a type alias, we can use this in the type signatures:
type alias Customer =
{ name: String
, age: CustomerAge
}
customer : Customer
customer = ...
getName : Customer -> String
getName customer = ...
The type alias tells the Elm compiler that a Customer is a record with a field name of type String, and a field age of the type CustomerAge (that we defined earlier).
Imagine calling the getName function with an object without a name field.
In JavaScript, this would obviously crash hard, but in Elm - the code won't even compile!
This moves the discovery of errors from runtime to compile time (when you hit save in your editor), which significantly improves our feedback cycle!
Task: Create a type alias called Card that describes our card record. Use this new type in the signatures of viewCard and myCard.
2.6 Render all the states!
Our cards can be either Open, Closed or Matched, and we want to display each state differently.
For this we will be using a language feature called pattern matching.
It can best be described as a switch-statement on steroids, allowing us to do more than simple matching on a value.
Example:
isAdult : CustomerAge -> Bool
isAdult customerAge =
case customerAge of
Known age ->
age > 18
Unknown ->
False
Notice that we can even extract the value that was used when Known : Int -> CustomerAge was used!
This is a powerful technique, and is almost always used whenever there's a union type around.
In our case, it is handy for rendering different stuff based on the CardState of a card.
In viewCard, use the following logic (css classes should be applied to the img tag):
- When
Closed-> show/cats/closed.pngand the css classclosed - When
Open-> show/cats/{cardId}.pngand the css classopen - When
Matched-> show/cats/{cardId}.pngand the css classmatched
Having only one card is pretty boring and we won't to be able to see all the different states, so let's create a list of them.
Lists in Elm is created with [], just like in JavaScript.
Put three cards in the list; one with id = 1, one with id = 2 and one with id = 3. Each should also have a different value for state.
Task:
- Update
viewCardto display differently based on the card'sstate - Create
myCards : List Card - Create
viewCards : List Card -> Html a- the cards should be placed in adivwith the css classcards - Call
viewCardsfrommain
Hint:
Use the built-in function List.map : (a -> b) -> List a -> List b to convert a list of Card to a list of Html a.
Remember that div : List (Attribute msg) -> List (Html a) -> Html a – notice the second argument (List (Html a))and how it corresponds with the return value of List.map.
Notice how the type signature helps in communicating what the function does! Type signatures are a very powerful tool, as you will discover throughout this workshop.
Make sure you render the correct image source for each card ({card.id}.png).