Skip to content

Commit 613e096

Browse files
authored
Type inference of all constructs and the next 15 months (#1814)
1 parent 23bd6c7 commit 613e096

File tree

1 file changed

+219
-0
lines changed

1 file changed

+219
-0
lines changed
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
---
2+
layout: post
3+
title: "Type inference of all constructs and the next 15 months"
4+
authors:
5+
- José Valim
6+
category: Announcements
7+
excerpt: "Today we celebrate 15 years since Elixir's first commit! To mark the occasion, we are glad to announce the first release candidate for Elixir v1.20, which performs type inference of all language constructs, with increasing precision."
8+
---
9+
10+
Today we celebrate 15 years since [Elixir's first commit](https://github.com/elixir-lang/elixir/commit/337c3f2d569a42ebd5fcab6fef18c5e012f9be5b)! To mark the occasion, we are glad to announce the first release candidate for Elixir v1.20, which performs type inference of all language constructs, with increasing precision.
11+
12+
In this blog post, we will break down exactly what this means, and what to expect in the short and medium term of the language evolution (roughly the next 15 months).
13+
14+
## Types, in my Elixir?
15+
16+
In 2022, [we announced the effort to add set-theoretic types to Elixir](/blog/2022/10/05/my-future-with-elixir-set-theoretic-types/). In June 2023, we [published an award winning paper on Elixir's type system design](https://arxiv.org/abs/2306.06391) and said our work was transitioning [from research to development](/blog/2023/06/22/type-system-updates-research-dev/).
17+
18+
Our goal is to introduce a type system which is:
19+
20+
* **sound** - the types inferred and assigned by the type system align with the behaviour of the program
21+
22+
* **gradual** - Elixir's type system includes the `dynamic()` type, which can be used when the type of a variable or expression is checked at runtime. In the absence of `dynamic()`, Elixir’s type system behaves as a static one
23+
24+
* **developer friendly** - the types are described, implemented, and composed using basic set operations: unions, intersections, and negations (hence it is a set-theoretic type system)
25+
26+
However, I want to emphasize what the gradual typing means in Elixir. Many gradual type systems have the `any()` type, which, from the point of view of the type system, often means "anything goes" and no type violations are reported.
27+
28+
On the other hand, Elixir's gradual type is called `dynamic()` and it works as a range. For example, you can say `dynamic(integer() or float())`, which means the type is either `integer() or float()` at runtime. Then if you proceed to pass it to a function that expects a `binary()`, you will get a typing violation. This allows the type system to emit warnings even in the presence of dynamism. Even if you declare a type as `dynamic()` and then proceed to use as `integer()` and then `binary()`, a type violation is still reported. We have also [developed new techniques that ensure our gradual typing is sound, without a need for additional runtime checks](/blog/2023/09/20/strong-arrows-gradual-typing/).
29+
30+
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), and [Tidewave](https://tidewave.ai/).
31+
32+
Let's see how this is turning out in practice.
33+
34+
## Inference across Elixir releases
35+
36+
Elixir v1.17 was the first release to introduce set-theoretic types into the compiler. Elixir v1.18 added inference of patterns and return types. Therefore, if you wrote this code:
37+
38+
```elixir
39+
defmodule User do
40+
defstruct [:age, :car_choice]
41+
42+
def drive(%User{age: age, car_choice: car}, car_choices) when age >= 18 do
43+
if car in car_choices do
44+
{:ok, car}
45+
else
46+
{:error, :no_choice}
47+
end
48+
end
49+
50+
def drive(%User{}, _car_choices) do
51+
{:error, :not_allowed}
52+
end
53+
end
54+
```
55+
56+
Elixir’s type system will infer the drive function expects a `User` struct as input and returns either `{:ok, dynamic()}` or `{:error, :no_choice}` or `{:error, :not_allowed}`. Therefore, the following code
57+
58+
```elixir
59+
User.drive({:ok, %User{}}, car_choices)
60+
```
61+
62+
will emit a warning stating that we are passing an invalid argument, both in your IDE and the shell:
63+
64+
![Example of a warning when passing wrong argument to a function](/images/contents/type-warning-function-clause.png)
65+
66+
Now consider the expression below. We are expecting the `User.drive/2` call to return `:error`, which cannot possibly be true:
67+
68+
```elixir
69+
case User.drive(user, car_choices) do
70+
{:ok, car} -> car
71+
:error -> Logger.error("User cannot drive")
72+
end
73+
```
74+
75+
Therefore the code above would emit the following warning:
76+
77+
![Example of a warning when a case clause won't ever match](/images/contents/type-warning-case.png)
78+
79+
However, Elixir v1.18 could only infer types from patterns. If you wrote this code:
80+
81+
```elixir
82+
def user_age_to_string(user) do
83+
Integer.to_string(user.age)
84+
end
85+
```
86+
87+
Elixir would not infer anything about the function arguments. As of Elixir v1.20-rc, Elixir correctly infers the function to be `%{..., age: integer()} -> binary()`, which means it expects a map with at least the `age` field (the leading `...` indicates other keys may be present) and it returns a `binary()`.
88+
89+
Or let's see another example:
90+
91+
```elixir
92+
def add_rem(a, b) do
93+
rem(a + b, 8)
94+
end
95+
```
96+
97+
While `a + b` works with both integers and floats, because the `rem` (remainder) function works exclusively with integers, Elixir correctly infers that `a` and `b` must also both be integers. If you try calling the function above with a float, you will also get a type violation.
98+
99+
In a nutshell, we have been steadily increasing the amount of inference in Elixir programs. Our goal is to find typing violations in Elixir programs for free, without a need for developers to change existing code. And, in the last few days, we finally wrapped up the last missing piece.
100+
101+
## Inference of guards
102+
103+
Elixir v1.20-rc also performs inference of guards! Let's see some examples:
104+
105+
```elixir
106+
def example(x, y) when is_list(x) and is_integer(y)
107+
```
108+
109+
The code above correctly infers `x` is a list and `y` is an integer.
110+
111+
```elixir
112+
def example({:ok, x} = y) when is_binary(x) or is_integer(x)
113+
```
114+
115+
The one above infers x is a binary or an integer, and `y` is a two element tuple with `:ok` as first element and a binary or integer as second.
116+
117+
```elixir
118+
def example(x) when is_map_key(x, :foo)
119+
```
120+
121+
The code above infers `x` is a map which has the `:foo` key, represented as `%{..., foo: dynamic()}`. Remember the leading `...` indicates the map may have other keys.
122+
123+
```elixir
124+
def example(x) when not is_map_key(x, :foo)
125+
```
126+
127+
And the code above infers `x` does not have the `:foo` key (hence `x.foo` will raise a typing violation), which has the type: `%{..., foo: not_set()}`.
128+
129+
You can also have expressions that assert on the size of data structures:
130+
131+
```elixir
132+
def example(x) when tuple_size(x) < 3
133+
```
134+
135+
Elixir will correctly track that the tuple has at most two elements, and therefore accessing `elem(x, 3)` will emit a typing violation. In other words, Elixir can look at complex guards, infer types, and use this information to find bugs in our code!
136+
137+
## The next ~15 weeks
138+
139+
As we work on the type system, we have been carefully monitoring the compiler performance. And while we have been able to [develop new techniques to keep everything running smoothly](/blog/2025/12/02/lazier-bdds-for-set-theoretic-types/), the next weeks will dramatically ramp up the amount of type information flowing through the compiler, and therefore we need your feedback.
140+
141+
The next Elixir release is scheduled for May. We are shipping this release candidate earlier than usual for validation. We also plan to launch _at least two additional release candidates_ with increased type checking.
142+
143+
### Jan/2026: inference of all constructs
144+
145+
The first release candidate is out right now, with type inference of all Elixir constructs. Please give it a try. However, at this stage, we expect some false positives: the type system will report warnings which are not actual violations. We will explain exactly why in the next paragraphs. So don't change your programs yet. The most valuable feedback we want from you is performance! If everything compiles at roughly the same speed as before, then hooray!
146+
147+
### Feb-Mar/2026: inference across clauses
148+
149+
The second release candidate will add type inference across clauses. Let's see some examples. Take this code:
150+
151+
```elixir
152+
case some_function_call() do
153+
%{name: name} = user -> ...
154+
%{first_name: first, last_name: last} = user ->
155+
end
156+
```
157+
158+
Today, we know `user` in the first clause has the `name` field (and potentially other fields). We know that `user` in the second clause has `first_name` and `last_name`. The code above also implies that `user` in the second clause **does not** have the `name` field (after all, if it had the `name` field, the first clause would have matched). In other words, pattern matching order becomes a source of negative type information. In the first release candidate, the type system cannot infer this information yet, but it will be implemented in the following release candidate.
159+
160+
Besides giving us more precise types, the above will also allow us to perform exhaustiveness checks as well as find redundant clauses (note we already warn for clauses that won't ever match since Elixir v1.18).
161+
162+
However, it is worth keeping in mind the work is a bit more complex than one might think. For example, take this code:
163+
164+
```elixir
165+
case some_function_call() do
166+
%{age: age} = user when age >= 21 -> ...
167+
%{name: name} = user ->
168+
end
169+
```
170+
171+
Can we say the `user` in the second clause does not have the `age` field? No, we can't, because the first clause only matches if age is greater than or equal to 21. So the second clause will still match users with a lower age. This means we must distinguish between "surely accepted clauses" and "potentially accepted clauses".
172+
173+
### Apr-May/2026: inference across dependencies
174+
175+
Finally, we will ship a third release candidate, which enables type inference for function calls across your dependencies. In the current release candidate, Elixir can infer types from function calls, but such inference only applies to modules from Elixir's standard library. Take the following code:
176+
177+
```elixir
178+
def integer_to_string(x) do
179+
Integer.to_string(x)
180+
end
181+
```
182+
183+
In the code above, we will infer `x` is an `integer()`, but if instead you call `MyInteger.to_string(x)` from a dependency, we only perform type checking, we won't infer the `integer_to_string` function expects an integer. Once implemented, this step will drastically increase the amount of types flowing through the compiler, hence we are dedicating a release candidate for it.
184+
185+
## The next ~15 months
186+
187+
At this point, you may be wondering: when can we officially claim Elixir is statically typed?
188+
189+
When we [first announced the type system effort](/blog/2022/10/05/my-future-with-elixir-set-theoretic-types/), we broke it into three distinct milestones:
190+
191+
1. Type inference of patterns and guards: this is our current milestone which has, since then, been extended to type inference of all language constructs
192+
193+
2. Introduction of typed structs, allowing struct types to propagate throughout the system, as we pattern match on structs throughout the codebase
194+
195+
3. Introduction of type signatures, including for parametric and protocol polymorphism
196+
197+
Assuming all release candidates above go according to plan, we will officially conclude the first milestone as part of Elixir v1.20 and start working on the subsequent ones. However, there are still challenges ahead that may prove the type system to be impractical:
198+
199+
* Ergonomics: all of our improvements so far have happened behind the scenes, without changes to the language. While this has been very valuable to validate the feasibility and performance of the type system, we still need to assess its impact on the developer experience
200+
201+
* Performance: our current implementation does not yet support recursive and parametric types and those may also directly impact performance and make the type system unfeasible
202+
203+
Our goal is to explore these problems and their solutions in the future Elixir v1.21 (Nov/2026) and v1.22 (May/2027) releases, by implementing these operations in the compiler and using it to internally type complex Elixir modules, such as the `Enum` module. So while we don't have a precise date for when we will conclude these upcoming milestones, we will likely continue to see gradual improvements on every release for the next 15 months.
204+
205+
## Wrapping up
206+
207+
The first release candidate for Elixir v1.20 is out and includes type inference of all constructs. We will have multiple release candidates before the final release in May/2026, and your feedback is very important:
208+
209+
* Jan/2026: inference of all constructs, may have many false positives, assess performance!
210+
* Feb-Mar/2026: inference across clauses, few or none false positives, assess performance!
211+
* Apr-May/2026: inference across dependencies, assess performance!
212+
213+
Every release will have a thread in the [Elixir Forum](http://elixirforum.com) for discussion.
214+
215+
Check our documentation to learn more about our [overall work on set-theoretic types](http://hexdocs.pm/elixir/1.20.0-rc.0/gradual-set-theoretic-types.html). This release also includes [our official types cheatsheet](https://hexdocs.pm/elixir/1.20.0-rc.0/types-cheat.html).
216+
217+
The [complete CHANGELOG for this release](https://github.com/elixir-lang/elixir/blob/v1.20.0-rc.0/CHANGELOG.md) is on GitHub.
218+
219+
Happy coding!

0 commit comments

Comments
 (0)