🧐 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
MyCalendarwhich has anensure_valid_weekfunction that returns:okor:errorif the week is less than 52 weeks. And, - An
Exercisemodule that usesMyCalendarto 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.