🧐 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 an ensure_valid_week function that returns :ok or :error if the week is less than 52 weeks. And,
  • An Exercise module that uses MyCalendar 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.

Want the latest Elixir Streams in your inbox?

    No spam. Unsubscribe any time.