Gradual Typing With Elixir

Elixir isn’t a statically typed language but with structs, schemas, pattern matching and a robust, built-in test suite you have a toolbox that allows for gradual typing.

For this example, we’ll walk through mock creating a user login with the parameters username, email_address and age. Rather than save the login to a database, we'll just use the parameters to print the following message: username: #{params["username"]}, email_address: #{params["email_address"]}, next_year_age: #{next_year_age} where next_year_age is age plus one.

With Just Params

If we were using the Phoenix Framework, our controller request would consists of a map with strings as keys: %{"username" => "waldo", "email_address" => "waldo@where.com", "age" => 12 so we’ll use this as our payload in the following examples.

To print our message, we would need to add 1 to age and parse each property from the map.

The advantage of this method is that it's fast and easily readable. However, there are a couple potential issues that a statically typed languages would catch at compile time. First, we can pass any type (a map, a string, an integer) into print as params so there is no contractual guarantee that print would ever receive a map with the properties "username", "email_address", or "age” in a runtime error. We also have no guarantee that params["age"] is going to be a number. If "age" is passed in as a string we'll run into an ArithmeticError at runtime.

Check here.

With a Login Struct

Elixir has structs which will give our code some more structure. Let’s define a Login struct and see how we can add some guarantees to our code.

Here we define a Login struct with the fields username, age and email_address and use cast_login to cast paramsto a Login struct. Although we still don't have any guarantees about Login's properties' types, we get the guarantee that those properties will be present. The biggest benefit of defining Login is that, with pattern matching, we can guarantee that print will only execute if it receives the Login struct. If anything else is passed in at runtime it will result in an error.

To help with the issue where any type can be passed into cast_login, we introduce when along with is_map to ensure that cast_login will run only if it receives a map. There are still no guarantees that we will receive the username, email_address and age properties but at least it prevents a random string from being passed in. If you try to pass in a string like "waldo", you'll get a FunctionClauseError at runtime which looks like this in testing.

Code here

With BetterLogin Schema

Ecto is an Elixir dependency that facilitates database interaction. When coming from other ORMs, its take a while to get your head around Ecto which takes a more functional approach rather than an object oriented one and has it’s own Domain Specific Language. One characteristic of Ecto that sets it apart from other ORMs is that its components are loosely coupled and as a result, we can use Ecto.Schema to create "types" and help us cast our maps.

By using embedded_schema we now get some type guarantees of the fields in BetterLogin. If you try to instantiate BetterLogin with a string for the age field you will receive an error. Now when calculate next_year_age we can be assured that login.age will be an integer.

Using Ecto also gives us access to Ecto.Changeset which allows you to cast and validate your map to the schema as we do with marshal . Outside of taking over some of the grunt work associated with casting and validating parameters, Ecto.Changeset will also cast a string to the appropriate field type. For example, if we were to receive %{"age" => "42"} , the Ecto.Changeset.cast will cast the string “42” to an integer.

Code here

Conclusion

Elixir is often dismissed out of hand for larger projects because it isn’t statically typed. However, it does have built in features that provide you with many of the guarantees you get in a statically typed language and help tame large projects. You rarely get the best of both worlds but with Elixir you get pretty close to getting the benefits of being able to dynamic code while getting the guarantees of a statically typed language.