Elmchemy — Write type-safe Elixir code with Elm’s syntax — part 2 — Our own RPG character module

Written by krzysztof.wende | Published 2017/05/31
Tech Story Tags: functional-programming | elm | elm-lang | elixir | erlang

TLDRvia the TL;DR App

In this part of the article we’re going to write our own library using Elmchemy. If you haven’t read part one yet, it’s strongly advised. It covers the basics of starting a new and integrating an existing project with Elmchemy compiler.

During this tutorial we’ll learn how to:

  1. Define type aliases
  2. Define union types
  3. Use aliases and tagged unions as functions
  4. Pattern match on union types
  5. Use operators as functions and define your own custom ones
  6. Import types and type aliases from other modules

So imagine we’re writing a game. An amazingly innovative game. A game no-one has ever made before. With characters, levels, stats, and items kind of game. It is set in sci-fi-ish word so we can have crossbows, rifles, crowbars, blasters and what-not. Because we’re creating a block-buster and project managers rarely happen to have inconceivable deadline expectations, we were assigned a single task: our job is to create a module responsible for our main character — Gordon Freemonad.We’re a start-up and our salary is roughly 5 hundred bucks a month, so we were given the opportunity to plan out the features ourselves (Product manager would say it’s not what he wanted anyway).

We’re one lazy programmer and we’ll use a project created in part one. If you don’t have it, then you already failed. There goes your equity. As a first step we’ll create several scenarios testing unimplemented features, and then we’ll start thinking on how to implement them (look it up. It’s called TDD and it’s closest to gaming at work you can get without getting fired). Testing suite will be implemented in Elixir’s ExUnit exactly like in the previous part of the tutorial, everything else will be coded using Elmchemy only.

Enough talking. Time to start coding.

Preparation

*This article was written using Elmchemy 0.3.31. Make sure to have a version that is above 0.3.31 but no higher than 0.4*

Due to your wage and non-negotiable full-time amount of hours a week, you suffer from World Of Warcraft withdrawal. A lack of your both most hated and beloved game results in a pretty bold statement: Every game character has to have stats.

Let’s write a test that will check that new character has them.If you followed previous part your project structure should look like this:

Create a new file called character_test.exs inside test directory and start with a boilerplate:

test/character_test.exs - Ignore the error on `use Elmchemy`. Our plugin just can’t see dependencies that are installed as mix archives

Now what our character should have? Definitely a name and a surname. Having some gender would be nice too. Age is overrated so let’s leave that out. Not much but might be enough for our MVP.

test/character_test.exs

Cool. Next thing is we want him to have basic stats. Strength for damage, intelligence for weapon requirements and vitality for health should be enough for our MVP. We don’t yet know what will be the default values so let’s just check if the stats exists and are integers.

character_test.exs

We also check if we are able to set a stat for our character, using set_stat/3 function.

Stats would be nothing if they didn’t provide some benefit for having them:

Vitality boosts our health presented as {current_hp, max_hp} tuple:

test/character_test.exs — We check that max hp increased, and also that our current hp adjusted itself accordingly

In our test we create a new Gordon and clone two copies of him. One beefed up with 10 vitality, and the other who’s a programmer like us - with 0. Then we compare their health points and expect the former to have more of them.In the last line we make sure that boosting Gordon’s max health, also raises his base health accordingly.

*Please note that we put the target of our function as last parameter, rather then the first one. That’s because in curried environment for pipes to work, we need the have the piping target as last argument — and that’s Elm’s and Elmchemy’s standard*

Intelligence allows us to equip more advanced weapons:

test/character_test.exs — We create two Gordons, a weapon with 9 intelligence requirement,

Here we create a new character and a weapon with level 9 and 100 damage.Then we get two clones of Gordon: - one with 10 Intelligence - and the other one with 0.We ran equip function on both of them, and expect it to fail on the character with 0 intelligence, but succeed on one with 10

At the end we make sure that the smarter clone of Gordon has a weapon in his arm. Please notice that we wrap it in single tuple {weapon} , because that’s how Elmchemy represents [Maybe](http://package.elm-lang.org/packages/elm-lang/core/latest/Maybe) type. With {value} being Just value and nil being Nothing.

Implementation

Test #1 — Character name, surname and gender

Feature 1— Type aliases

Now that we have our tests we can start implementing our character!For our schema we’re going to use a type alias. It allows us to have a common name for any other type. We’re going to use it to alias a struct. Which Elmchemy represents as a map with atoms as keys.

Create a elm/Character.elm file with our type alias declaration.

elm/Character.elm

Type Alias Tip: If the type we’re aliasing is a struct or a tuple, we can use the alias as a function to create an instance of it with each argument being a each subsequent value in our type. f.i Character name surname gender health

We declare our Character to have a name and a surname, which are strings.A gender of type Gender which is a type we didn’t declare yet, and health as tuple, where the first one is current health, and the latter is max health.

Feature 2— Union Types

Whenever we wan’t something to be matched on, we want a custom type.In Elmchemy types are so called ‘Tagged unions’ which basically means, the first symbol in each type declaration is a ‘tag’ that tells us what type the value represents. Union types in Elmchemy are represented as atoms in case of a single tag, and a tuple starting with an atom in case of tags wrapping one or more type.That’s how we’re going to declare our gender

elm/Character.elm — We declare that gender can either be an atom :male, :female or :other

Feature 3— Type Aliases as functions

Now we can declare a new function that returns a new character based on it’s parameters.

elm/Character.elm — function returning our Character type alias intance

We take 3 parameters, which are name and surname as strings and a gender as our newly created Gender union type. Then we pass the arguments to Character type as a function in the order we defined the fields.We also pass the default argument of health being 100 on 100 max.

If we run mix test in our terminal right now we should have our first test passing. 3 to go!

Test #2 — Character stats

Time to add stats. Let’s recall our test case

character_test.exs

First we need to define stats for our character type.

And also define the stats structure:

When we save, our compiler should nag us, that our new/3 function is no longer relevant to our type.

Lets update it to contain default stats all being of value 0

Feature 4 — case statement on union types and record update syntax

We need a type of a stat to match on

Now we need to declare a function that takes a stat, value and character, and updates the stat to the value we want.

**Did you know?**You can define type aliases using record update syntax.type alias Namable a = { a | name : String} means any structure that has a field name of type String.You can then create types deriving from it like so:type alias Cat = Namable { lactoseIntolerance : Bool } ``type alias Dog = Namable { catIntolerance : Int } But remember, that way you sacrifice the short syntax for instantiating type aliases and have to type the entire struct by hand.

Great. That’s another test down!

Test #3 — Boosting vitality

test/character_test.exs — We check that max hp increased, and also that our current hp adjusted itself accordingly

Now we need to add the functionality of changing health when setting a vitality stat.

Feature 5— Operators as functions and custom operators

Because our HP is a tuple we want to use Tuple.map on it. But for the sake of code tidiness we’re going to define an operator for us to use that as infix operator.

We could use any operator i like, but for the sake of the shape I chose <$ for the left tuple element and$> for the right one. You can experiment with what works best for you.

Did you know?There is plenty of built in operators to make your life easier. You can use |> and <| to pipe function results, << and >> to compose functions. There even is a comma operator that creates tuples for you. (,) a b is equivalent to (a, b) (,,) a b c to (a, b, c) and so on.Every operator is still a function so we can pass them to other functions to make our code much more expressive.All it takes to implement a zip of two lists is just to writeList.map2 (,) listA listB

Great. Now we can add the health change to the Vitality branch of our setStat case.

We use <$ to add a difference of current vitality and a new vitality value multiplied by 10.Then we use $> to set it to base 100 plus 10 times the vitality value.Since we need to pass it a function in the first case we give it a (+) function and in the second we give it always which basically returns always the same thing (Basically it means the same as\_ -> (100 + 10 * value))If we didn’t use our operator the code would look as follows

character.health|> Tuple.mapFirst ((+) ((value - character.stats.vitality) * 10))|> Tuple.mapSecond (always (100 + 10 * value))

By now we should be with only one test failing left.

Test #4 — Equipping a weapon when we’re intelligent enough

test/character_test.exs - a weapon with 9 intelligence requirement,

So we need a function, that either can or cannot equip a weapon. To do that we’re going to use Result a b type, that have a value of either Err a or Ok b, which directly translates to {:ok, a} or {:error, b}.

Please notice the difference of Err translating to :error instead of :err. That’s one of exceptions in Elmchemy to keep Elixir and Elm interoperable without a consistency sacrifice

But before we do that, we need to implement a Weapon type in general. To do that we’re create a new file, and learn to import and use remote types.

Feature 6 — Union types and Type Aliases imported from other modules

Let’s create a new elm/Weapon.elm file and add a new function that would create a weapon for us

elm/Weapon.elm — weapon type definition

*Please note that we create a _new_ function only for the sake of usage inside our Elixir code. The _Weapon_ type alias is a factory function itself when used inside Elmchemy’s scope. _Character_ factory function (_Character.new_) was different because we specified some default values for it*

Now that we have our Weapon type we can implement our equip function, but first we need to add an import on the top of our Character.elm file, we use import Weapon exposing (Weapon) because the first one is the name of the module, and the latter of the type alias we want to import.

Also our character is right now armless. Let’s add it an arm, of type Maybe Weapon. Like we decided earlier we want to use Maybe type, because our character can either have something in his hand or have nothing at all.Let’s add an arm to our Character type with

**Did you know?**There is a shorthand syntax to access a field in a struct as a function.By writing .field you can have a function that will fetch field of a record. It’s equivalent to \a -> a.field For example:arsenal : List Character -> List Weaponarsenal squad = List.map .arm _squad_

We need to add a default value for arm to our Character.new function too.

elm/Character.elm

Now that we have the type imported from the other file and an arm for our character we can go about our weapon equipping implementation:

elm/Character.elm

That’s it! If we run the tests now, we should see all 4 tests green.

If you skipped some parts you can see the entire project under this repository:https://github.com/wende/elmchemy-article-example

This is the end of part twoIn part three we will focus on calling Elixir code from Elmchemy and writing your own Native modules.

In case of any questions regarding the project I’ll gladly answer in the comments section.

Announcement:

Elmchemy is undergoing a name change and is soon to be called “Elchemy” (without an “m”)

<<< Part One

— Part Three >>>


Published by HackerNoon on 2017/05/31