🧐 Taking Elixir 1.18's new type system changes for a spin
Notes
Elixir 1.18 ships with “type checking of function calls, alongside gradual inference of patterns and return types.”
I’m particularly excited about that because of the help the compiler can provide when changing return values for a function.
Consider the following two modules:
- A
MyCalendar
which has anensure_valid_week
function that returns:ok
or:error
if the week is less than 52 weeks. And, - An
Exercise
module that usesMyCalendar
to validate a week, returning a success or error message.
defmodule MyCalendar do
@weeks_in_a_year 52
def ensure_valid_week(value) when is_integer(value) do
if value <= @weeks_in_a_year do
:ok
else
:error
end
end
end
defmodule Exercise do
def do_something do
week = 2
case MyCalendar.ensure_valid_week(week) do
:ok -> "We did it!"
:error -> "Oh, oh! That's not a valid week."
end
end
end
It’s a contrived example, but I think you get the idea.
Now, what happens if we change the error return value in ensure_valid_week
from
:error
to {:error, :invalid_week}
, but we don’t update the pattern match in
the Exercise
module?
defmodule MyCalendar do
@weeks_in_a_year 52
def ensure_valid_week(value) when is_integer(value) do
if value <= @weeks_in_a_year do
:ok
else
+ {:error, :invalid_week}
- :error
end
end
end
If we’re using Elixir 1.17, our compiler won’t tell us anything about the change. We hope our tests catch the error before we ship the code to production.
But if we’re using Elixir 1.18, when we compile, we get a beautiful warning:
$ mix compile
Compiling 1 file (.ex)
warning: the following clause will never match:
:error
because it attempts to match on the result of:
MyCalendar.ensure_valid_week(week)
which has type:
dynamic(:ok or {:error, :invalid_week})
typing violation found at:
│
7 │ :error -> "Oh, oh! That's not a valid week."
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
│
└─ lib/exercise.ex:7: Exercise.do_something/0
🤩 That’s a huge improvement that we, as Elixir users, get for free!
Of course, it’s not perfect yet. If we don’t reference the MyCalendar
module
directly, but instead make it the default argument that we can pass into the
function (i.e. only making it one of the possible calendar implementations),
then the compiler doesn’t know for sure that the :error
value won’t match.
To see that, change the MyCalendar.ensure_valid_week(week)
call and make
MyCalendar
the default argument for the function:
defmodule Exercise do
- def do_something do
+ def do_something(calendar \\ MyCalendar) do
week = 2
- case MyCalendar.ensure_valid_week(week) do
+ case calendar.ensure_valid_week(week) do
:ok -> "We did it!"
:error -> "Oh, oh! That's not a valid week."
end
end
end
Now, mix compile
in Elixir 1.18 will not give us a warning.
But that makes sense. It’s now possible to pass a calendar
argument that does
return ``:error -- so there are scenarios in which the
:error pattern would
match. The compiler just doesn't know anymore that
MyCalendar` is the only
possible implementation.
It would be amazing if the compiler could figure out what are all the call sites
to do_something/1
, calculate all their return values, and let us know if
:error
is ever going to be returned. But I don’t know if that’s in the cards.
For now, I’ll happily take the huuuuge improvement.