Skip to content

Commit 9e7da92

Browse files
authored
Elixir v1.18.0 announcement (#1783)
1 parent b0c034a commit 9e7da92

9 files changed

+222
-5
lines changed

.ruby-version

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.0.7

Gemfile

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,4 @@ source 'https://rubygems.org'
22

33
gem 'github-pages'
44
gem 'json', '>= 2.0.0'
5-
6-
gem "webrick", "~> 1.8"
5+
gem 'webrick', '>= 1.8'

Gemfile.lock

+2-2
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ PLATFORMS
258258
DEPENDENCIES
259259
github-pages
260260
json (>= 2.0.0)
261-
webrick (~> 1.8)
261+
webrick (>= 1.8)
262262

263263
BUNDLED WITH
264-
2.3.7
264+
2.5.23

_includes/search.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<div class="widget news">
2-
<h3>News: <a href="/blog/2024/06/12/elixir-v1-17-0-released/">Elixir v1.17 released</a></h3>
2+
<h3>News: <a href="/blog/2024/12/19/elixir-v1-18-0-released/">Elixir v1.18 released</a></h3>
33
</div>
44

55
<div class="widget search">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
---
2+
layout: post
3+
title: "Elixir v1.18 released: type checking of function calls, Language Server listeners, built-in JSON, and ExUnit improvements"
4+
author: José Valim
5+
category: Releases
6+
excerpt: "Elixir v1.18 released: type checking of function calls, Language Server listeners, built-in JSON, and ExUnit improvements"
7+
---
8+
9+
Elixir v1.18 is an impressive release with improvements across the two main efforts happening within the Elixir ecosystem right now: set-theoretic types and language servers. It also comes with built-in JSON support and adds new capabilities to its unit testing library. Let's go over each of those in detail.
10+
11+
## Type inference of patterns and return types
12+
13+
There are several updates in the typing department, so let's break them down.
14+
15+
#### A type system? In my Elixir?
16+
17+
There is an on-going [research and development](https://elixir-lang.org/blog/2023/06/22/type-system-updates-research-dev/) effort to bring static types to Elixir. Elixir's type system is:
18+
19+
* **sound** - the types inferred and assigned by the type system align with the behaviour of the program
20+
21+
* **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
22+
23+
* **developer friendly** - the types are described, implemented, and composed using basic set operations: unions, intersections, and negation (hence it is a set-theoretic type system)
24+
25+
More interestingly, you can compose `dynamic()` with any type. For example, `dynamic(integer() or float())` means the type is either `integer()` or `float()` at runtime. This allows the type system to emit warnings if none of the types are satisfied, even in the presence of dynamism.
26+
27+
#### What has already been done?
28+
29+
[Elixir v1.17 was the first release to incorporate the type system in the compiler](https://elixir-lang.org/blog/2024/06/12/elixir-v1-17-0-released/). In particular, we have added support for primitive types (integer, float, binary, pids, references, ports), atoms, and maps. We also added type checking to a handful of operations related to those types, such as accessing fields in maps, as in `user.adress` (mind the typo), performing structural comparisons between structs, as in `my_date < ~D[2010-04-17]`, etc.
30+
31+
#### What is new in v1.18?
32+
33+
The most exciting change in Elixir v1.18 is type checking of function calls, alongside gradual inference of patterns and return types. To understand how this will impact your programs, consider the following code defined in `lib/user.ex`:
34+
35+
```elixir
36+
defmodule User do
37+
defstruct [:age, :car_choice]
38+
39+
def drive(%User{age: age, car_choice: car}, car_choices) when age >= 18 do
40+
if car in car_choices do
41+
{:ok, car}
42+
else
43+
{:error, :no_choice}
44+
end
45+
end
46+
47+
def drive(%User{}, _car_choices) do
48+
{:error, :not_allowed}
49+
end
50+
end
51+
```
52+
53+
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
54+
55+
```elixir
56+
User.drive({:ok, %User{}}, car_choices)
57+
```
58+
59+
will emit a warning stating that we are passing an invalid argument:
60+
61+
![Example of a warning when passing wrong argument to a function](/images/contents/type-warning-function-clause.png)
62+
63+
Now consider the expression below. We are expecting the `User.drive/2` call to return `:error`, which cannot possibly be true:
64+
65+
```elixir
66+
case User.drive(user, car_choices) do
67+
{:ok, car} -> car
68+
:error -> Logger.error("User cannot drive")
69+
end
70+
```
71+
72+
Therefore the code above would emit the following warning:
73+
74+
![Example of a warning when a case clause won't ever match](/images/contents/type-warning-case.png)
75+
76+
Our goal is for the warnings to provide enough contextual information that lead to clear reports and that's an area we are actively looking for feedback. If you receive a warning that is unclear, please open up a bug report.
77+
78+
Elixir v1.18 also augments the type system with support for tuples and lists, plus type checking of almost all Elixir language constructs, except `for`-comprehensions, `with`, and closures. Here is a non-exaustive list of the new violations that can be detected by the type system:
79+
80+
* if you define a pattern that will never match any argument, such as `def function(x = y, x = :foo, y = :bar)`
81+
82+
* matching or accessing tuples at an invalid index, such as `elem(two_element_tuple, 2)`
83+
84+
* if you have a branch in a `try` that will never match the given expression
85+
86+
* if you have a branch in a `cond` that always passes (except the last one) or always fails
87+
88+
* if you attempt to use the return value of a call to `raise/2` (which by definition returns no value)
89+
90+
In summary, this release takes us further in our journey of providing type checking and type inference of existing Elixir programs, without requiring Elixir developers to explicitly add type annotations.
91+
92+
For existing codebases with reasonable code coverage, most type system reports will come from uncovering dead code - code which won't ever be executed - as seen in a [few](https://github.com/phoenixframework/phoenix_live_view/commit/6c6e2aaf6a01957cc6bb8a27d2513bff273e8ca2) [distinct](https://github.com/elixir-ecto/postgrex/commit/3308f277f455ec64f2d0d7be6263f77f295b1325) [projects](https://github.com/phoenixframework/flame/commit/0c0c2875e42952d2691cbdb7928fc32f4715e746). A notable example is the type system ability to track how private functions are used throughout a module and then point out which clauses are unused:
93+
94+
```elixir
95+
defmodule Example do
96+
def public(x) do
97+
private(Integer.parse(x))
98+
end
99+
100+
defp private(nil), do: nil
101+
defp private("foo"), do: "foo"
102+
defp private({int, _rest}), do: int
103+
defp private(:error), do: 0
104+
defp private("bar"), do: "bar"
105+
end
106+
```
107+
108+
![Example of a warning for unused private clauses](/images/contents/type-warning-private.png)
109+
110+
Keep in mind the current implementation does not perform type inference of guards yet, which is an important source of typing information in programs. There is a lot the type system can learn about our codebases, that it does not yet. This brings us to the next topic.
111+
112+
#### Future work
113+
114+
The next Elixir release should improve the typing of maps, tuples, and closures, allowing us to type even more constructs. We also plan to fully type the `with` construct, `for`-comprehensions, as well as protocols.
115+
116+
But more importantly, we want to focus on complete type inference of guards, which in turn will allow us to explore ideas such as redundant pattern matching clauses and exhaustiveness checks. Our goal with inference is to strike the right balance between developer experience, compilation times, and the ability of finding provable errors in existing codebases. You can learn more [about the trade-offs we made for inference in our documentation](https://hexdocs.pm/elixir/1.18/gradual-set-theoretic-types.html#type-inference).
117+
118+
Future Elixir versions will introduce user-supplied type signatures, which should bring the benefits of a static type system without relying on inference. [Check our previous article on the overall milestones for more information](https://elixir-lang.org/blog/2023/06/22/type-system-updates-research-dev/).
119+
120+
#### Sponsors
121+
122+
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/) ([they are hiring!](https://www.fresha.com/careers/openings?department=engineering)), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
123+
124+
## Language server listeners
125+
126+
Three months ago, we welcomed [the Official Language Server team](https://elixir-lang.org/blog/2024/08/15/welcome-elixir-language-server-team/), with the goal of unifying the efforts behind code intelligence, tools, and editors in Elixir. Elixir v1.18 brings new features on this front by introducing locks and listeners to its compilation. Let's understand what it means.
127+
128+
At the moment, all language server implementations have their own compilation environment. This means that your project and dependencies during development are compiled once, for your own use, and then again for the language server. This duplicate effort could cause the language server experience to lag, when it could be relying on the already compiled artifacts of your project.
129+
130+
This release addresses the issue by introducing a compiler lock, ensuring that only a single operating system running Elixir compiles your project at a given moment, and by providing the ability for one operating system process to listen to the compilation results of others. In other words, different Elixir instances can now communicate over the same compilation build, instead of racing each other.
131+
132+
These enhancements do not only improve editor tooling, but they also directly benefit projects like IEx and Phoenix. Here is a quick snippet showing how to enable auto-reloading inside IEx, then running `mix compile` in one shell automatically reloads the module inside the IEx session:
133+
134+
<video controls>
135+
<source src="/images/contents/iex-auto-reload.mp4" type="video/mp4">
136+
Your browser does not support the video tag.
137+
</video>
138+
139+
## Built-in JSON
140+
141+
[Erlang/OTP 27 added built-in support for JSON](https://www.erlang.org/doc/apps/stdlib/json.html) and we are now bringing it to Elixir. A new module, called [`JSON`](https://hexdocs.pm/elixir/1.18/JSON.html), has been added with functions to encode and decode JSON. Its most basic APIs reflect the ones [from the Jason project](https://hexdocs.pm/jason/Jason.html) (the de-facto JSON library in the Elixir community up to this point).
142+
143+
A new protocol, called [`JSON.Encoder`](https://hexdocs.pm/elixir/1.18/JSON.Encoder.html), is also provided for those who want to customize how their own data types are encoded to JSON. You can also derive protocols for structs, with a single-line of code:
144+
145+
```elixir
146+
@derive {JSON.Encoder, only: [:id, :name]}
147+
defstruct [:id, :name, :email]
148+
```
149+
150+
The deriving API mirrors the one from `Jason`, helping those who want to migrate to the new `JSON` module.
151+
152+
## Parameterized tests and ExUnit groups
153+
154+
[ExUnit now supports parameterized tests](https://hexdocs.pm/ex_unit/1.18/ExUnit.Case.html#module-parameterized-tests). This allows your test modules to run multiple times under different parameters.
155+
156+
For example, Elixir ships [a local, decentralized and scalable key-value process storage called `Registry`](https://hexdocs.pm/elixir/Registry.html). The registry can be partitioned and its implementation differs depending if partitioning is enabled or not. Therefore, during tests, we want to ensure both modes are exercised. With Elixir v1.18, we can achieve this by writing:
157+
158+
```elixir
159+
defmodule Registry.Test do
160+
use ExUnit.Case,
161+
async: true,
162+
parameterize: [
163+
%{partitions: 1},
164+
%{partitions: 8}
165+
]
166+
167+
# ... the actual tests ...
168+
end
169+
```
170+
171+
Once specified, the number of partitions is available as part of the test configuration. For example, to start one registry per test with the correct number of partitions, you can write:
172+
173+
```elixir
174+
setup config do
175+
partitions = config.partitions
176+
name = :"#{config.test}_#{partitions}"
177+
opts = [keys: :unique, name: name, partitions: partitions]
178+
start_supervised!({Registry, opts})
179+
opts
180+
end
181+
```
182+
183+
Prior to parameterized types, Elixir resorted on code generation, which increased compilation types. Furthermore, ExUnit parameterizes the whole test modules, which also allows the different parameters to run concurrently if the `async: true` option is given. Overall, this features allows you to compile and run multiple scenarios more efficiently.
184+
185+
Finally, ExUnit also comes with the ability of specifying test groups. While ExUnit supports running tests concurrently, those tests must not have shared state between them. However, in large applications, it may be common for some tests to depend on some shared state, and other tests to depend on a completely separate state. For example, part of your tests may depend on Cassandra, while others depend on Redis. Prior to Elixir v1.18, these tests could not run concurrently, but in v1.18 they might as long as they are assigned to different groups:
186+
187+
```elixir
188+
defmodule MyApp.PGTest do
189+
use ExUnit.Case, async: true, group: :pg
190+
191+
# ...
192+
end
193+
```
194+
195+
Tests modules within the same group do not run concurrently, but across groups, they might.
196+
197+
With features like async tests, suite partitioning, and now grouping, Elixir developers have plenty of flexibility to make the most use of their machine resources, both in development and in CI.
198+
199+
## `mix format --migrate`
200+
201+
The `mix format` command now supports an explicit `--migrate` flag, which will convert constructs that have been deprecated in Elixir to their latest version. Because this flag rewrites the AST, it is not guaranteed the migrated format will always be valid when used in combination with macros that also perform AST rewriting.
202+
203+
As of this release, the following migrations are executed:
204+
205+
* Normalize parens in bitstring modifiers - it removes unnecessary parentheses in known bitstring modifiers, for example `<<foo::binary()>>` becomes `<<foo::binary>>`, or adds parentheses for custom modifiers, where `<<foo::custom_type>>` becomes `<<foo::custom_type()>>`.
206+
207+
* Charlists as sigils - formats charlists as `~c` sigils, for example `'foo'` becomes `~c"foo"`.
208+
209+
* `unless` as negated `if`s - rewrites `unless` expressions using `if` with a negated condition, for example `unless foo do` becomes `if !foo do`. We plan to deprecate `unless` in future relases.
210+
211+
More migrations will be added in future releases to help us push towards more consistent codebases.
212+
213+
## Summary
214+
215+
Other notable changes include [`PartitionSupervisor.resize!/2`](https://hexdocs.pm/elixir/1.18/PartitionSupervisor.html#resize!/2), for resizing the number of partitions (aka processes) of a supervisor at runtime, [Registry.lock/3](https://hexdocs.pm/elixir/1.18/Registry.html#lock/3) for simple in-process key locks, PowerShell versions of `elixir` and `elixirc` scripts for better DX on Windows, and more. [See the CHANGELOG](https://hexdocs.pm/elixir/1.18/changelog.html) for the complete release notes.
216+
217+
Happy coding!

images/contents/iex-auto-reload.mp4

740 KB
Binary file not shown.

images/contents/type-warning-case.png

902 KB
Loading
Loading
649 KB
Loading

0 commit comments

Comments
 (0)