Skip to content

Commit 22d6d03

Browse files
committed
Docs
1 parent 72dc7ed commit 22d6d03

File tree

6 files changed

+276
-28
lines changed

6 files changed

+276
-28
lines changed

lib/elixir/lib/module/types/descr.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2987,7 +2987,7 @@ defmodule Module.Types.Descr do
29872987

29882988
map_update_static_keys(dnf, required_keys, optional_keys, type, missing_fun, acc)
29892989

2990-
{_, [missing_domain | _], _} ->
2990+
{_, _, [missing_domain | _], _} ->
29912991
{:baddomain, domain_key_to_descr(missing_domain)}
29922992
end
29932993
end

lib/elixir/lib/module/types/expr.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,7 @@ defmodule Module.Types.Expr do
897897
message:
898898
IO.iodata_to_binary([
899899
"""
900-
expected a map with key #{to_quoted_string(key_type)} in map update syntax:
900+
expected a map with key of type #{to_quoted_string(key_type)} in map update syntax:
901901
902902
#{expr_to_string(expr, collapse_structs: false) |> indent(4)}
903903
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<!--
2+
SPDX-License-Identifier: Apache-2.0
3+
SPDX-FileCopyrightText: 2025 The Elixir Team
4+
-->
5+
6+
# Set-theoretic types cheatsheet
7+
8+
## Data types
9+
10+
### Indivisible types
11+
12+
```elixir
13+
binary()
14+
empty_list()
15+
integer()
16+
float()
17+
pid()
18+
port()
19+
reference()
20+
```
21+
22+
### Atoms
23+
24+
#### All atoms
25+
26+
```elixir
27+
atom()
28+
```
29+
30+
#### Individual atoms
31+
32+
```elixir
33+
:ok
34+
:error
35+
SomeModule
36+
```
37+
38+
### Functions
39+
40+
#### All functions
41+
42+
```elixir
43+
function()
44+
```
45+
46+
#### `n`-arity functions
47+
48+
```elixir
49+
(-> :ok)
50+
(integer() -> boolean())
51+
(binary(), binary() -> binary())
52+
```
53+
54+
#### Multiple clauses
55+
56+
```elixir
57+
(integer() -> binary()) and (binary() -> atom())
58+
```
59+
60+
### Maps
61+
62+
#### All maps
63+
64+
```elixir
65+
map()
66+
```
67+
68+
#### Empty map
69+
70+
```elixir
71+
empty_map()
72+
```
73+
74+
#### Maps with atom keys
75+
76+
```elixir
77+
# Only has the keys name and age
78+
%{name: binary(), age: integer()}
79+
80+
# Has the name key and age is optional
81+
%{name: binary(), age: if_set(integer())}
82+
83+
# Has the keys name and age and may have other keys (open map)
84+
%{..., name: binary(), age: integer()}
85+
```
86+
87+
#### Maps with domain keys (domain keys are always treated as optional)
88+
89+
```elixir
90+
# Has atom and binary keys
91+
%{atom() => binary(), binary() => binary()}
92+
93+
# Has atom and binary keys and may have other keys (open map)
94+
%{..., atom() => binary(), binary() => binary()}
95+
```
96+
97+
### Non-empty lists
98+
99+
#### Proper lists
100+
101+
```elixir
102+
non_empty_list(elem_type)
103+
```
104+
105+
#### Improper lists (as long as `tail_type` does not include lists)
106+
107+
```elixir
108+
non_empty_list(elem_type, tail_type)
109+
```
110+
111+
### Tuples
112+
113+
#### All tuples
114+
115+
```elixir
116+
tuple()
117+
```
118+
119+
#### n-element tuples
120+
121+
```elixir
122+
{:ok, binary()}
123+
{:error, binary(), term()}
124+
{pid(), reference()}
125+
```
126+
127+
#### At least n-element tuples
128+
129+
```
130+
{binary(), binary(), ...}
131+
```
132+
133+
## Additional types for convenience
134+
135+
#### Booleans
136+
137+
```elixir
138+
boolean() = true or false
139+
```
140+
141+
#### Lists
142+
143+
```elixir
144+
list() = empty_list() or non_empty_list(term())
145+
list(a) = empty_list() or non_empty_list(a)
146+
list(a, b) = empty_list() or non_empty_list(a, b)
147+
```

lib/elixir/pages/references/gradual-set-theoretic-types.md

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,38 @@ The current milestone aims to infer types from existing programs and use them fo
1717

1818
## A gentle introduction
1919

20-
Types in Elixir are written using the type named followed by parentheses, such as `integer()` or `list(integer())`. The basic types in the language are: `atom()`, `binary()`, `integer()`, `float()`, `function()`, `list()` (and `improper_list()`), `map()`, `pid()`, `port()`, `reference()`, and `tuple()`.
20+
Types in Elixir are written using the type named followed by parentheses, such as `integer()` or `list(integer())`.
2121

22-
Many of the types above can also be written more precisely. We will discuss their syntax in detail later, but here are some examples:
22+
The basic types are:
23+
24+
```elixir
25+
atom()
26+
binary()
27+
empty_list()
28+
integer()
29+
float()
30+
function()
31+
map()
32+
non_empty_list(elem_type, tail_type)
33+
pid()
34+
port()
35+
reference()
36+
tuple()
37+
```
38+
39+
Many of the types above can also be written more precisely. We will discuss their syntax in the next sections, but here are two examples:
2340

2441
* While `atom()` represents all atoms, the atom `:ok` can also be represented in the type system as `:ok`
2542

2643
* While `tuple()` represents all tuples, you can specify the type of a two-element tuple where the first element is the atom `:ok` and the second is an integer as `{:ok, integer()}`
2744

28-
* While `function()` represents all functions, you can specify a function that receives an integer and returns a boolean as `(integer() -> boolean())`
29-
3045
There are also three special types: `none()` (represents an empty set), `term()` (represents all types), `dynamic()` (represents a range of the given types).
3146

3247
Given the types are set-theoretic, we can compose them using unions (`or`), intersections (`and`), and negations (`not`). For example, to say a function returns either atoms or integers, one could write: `atom() or integer()`.
3348

34-
Intersections will find the elements in common between the operands. For example, `atom() and integer()`, which in this case it becomes the empty set `none()`. You can combine intersections and negations to perform difference, for example, to say that a function expects all atoms, except `nil` (which is an atom), you could write: `atom() and not nil`.
49+
Intersections will find the elements in common between the operands. For example, `atom() and integer()`, which in this case is the empty set `none()`. You can combine intersections and negations to perform difference, for example, to say that a function expects all atoms, except `nil` (which is an atom), you could write: `atom() and not nil`.
50+
51+
You can find a complete reference in the [set-theoretic types cheatsheet](../cheatsheets/types-cheat.cheatmd).
3552

3653
## The syntax of data types
3754

@@ -63,24 +80,51 @@ Internally, Elixir represents the type `list(a)` as the union two distinct types
6380

6481
#### Improper lists
6582

66-
You can represent all _improper_ lists as `improper_list()`. Most times, however, an `improper_list` is built by passing a second argument to `non_empty_list`, which represents the type of the tail.
83+
While most developers will simply use `list(a)`, the type system can express all different representations of lists in Elixirby passing a second argument to `non_empty_list`, which represents the type of the tail.
6784

6885
A proper list is one where the tail is the empty list itself. The type `non_empty_list(integer())` is equivalent to `non_empty_list(integer(), empty_list())`.
6986

7087
If the `tail_type` is anything but a list, then we have an improper list. For example, the value `[1, 2 | 3]` would have the type `non_empty_list(integer(), integer())`.
7188

72-
While most developers will simply use `list(a)`, the type system can express all different representations of lists in Elixir. At the end of the day, `list()` and `improper_list()` are translations to the following constructs:
73-
74-
list() == empty_list() or non_empty_list(term())
75-
improper_list() == non_empty_list(term(), term() and not list())
89+
If you pass a list type as the tail, then the list type is merged into the element type. For example, `non_empty_list(integer(), list(binary()))` is the same as `non_empty_list(integer() or binary(), empty_list())`.
7690

7791
### Maps
7892

79-
You can represent all maps as `map()`. Maps may also be written using their literal syntax, such as `%{name: binary(), age: integer()}`, which outlines a map with exactly two keys, `:name` and `:age`, and values of type `binary()` and `integer()` respectively.
93+
You can represent all maps as `map()`.
94+
95+
Maps may also be written using their literal syntax, such as `%{name: binary(), age: integer()}`, which outlines a map with exactly two keys, `:name` and `:age`, and values of type `binary()` and `integer()` respectively.
96+
97+
A key may be marked as optional using the `if_set/1` operation on its value type. For example, `%{name: binary(), age: if_set(integer())}` is a map that certainly has the `:name` key but it may have the `:age` key (and if it has such key, its value type is `integer()`).
98+
99+
We say the maps above are "closed": they only support the keys explicitly defined. We can also mark a map as "open", by including `...` as its first element.
100+
101+
For example, the type `%{..., name: binary(), age: integer()}` means the keys `:name` and `:age` must exist, with their respective types, but any other key may also be present. In other words, `map()` is the same as `%{...}`. For the empty map, you may write `%{}`, although we recommend using `empty_map()` for clarity.
102+
103+
#### Domain types
80104

81-
We say the map above is a "closed" map: it only supports the two keys explicitly defined. We can also mark a map as "open", by including `...` as its last element. For example, the type `%{name: binary(), age: integer(), ...}` means the keys `:name` and `:age` must exist, with their respective types, but any other key may also be present. In other words, `map()` is the same as `%{...}`. For the empty map, you may write `%{}`, although we recommend using `empty_map()` for clarity.
105+
In the examples above, all map keys were atoms, but we can also use other types as map keys. For example:
106+
107+
```elixir
108+
# Closed map
109+
%{binary() or atom() => integer()}
110+
111+
# Open map
112+
%{..., binary() or atom() => integer()}
113+
```
114+
115+
Currently, the type system only tracks the top of each individial type as the domain keys. For example, if you say:
116+
117+
```elixir
118+
%{list(integer()) => integer(), list(binary()) => binary()}
119+
```
120+
121+
That's the same as:
122+
123+
```elixir
124+
%{list(term()) => integer() or binary()}
125+
```
82126

83-
Structs are closed maps with the `__struct__` key pointing to the struct name.
127+
Furthermore, it is important to note that domain keys are, by definition, optional. Whenever you have a `%{integer() => integer()}`and you try to fetch a key, we must assume the key may not exist (after all, it is not possible to store all integers as map keys as they are infinite).
84128

85129
### Functions
86130

@@ -158,4 +202,4 @@ The third milestone is to introduce set-theoretic type signatures for functions.
158202

159203
## Acknowledgements
160204

161-
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The research was partially supported by [Supabase](https://supabase.com/) and [Fresha](https://www.fresha.com/). The development work is sponsored by [Fresha](https://www.fresha.com/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
205+
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/).

lib/elixir/scripts/elixir_docs.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ canonical = System.fetch_env!("CANONICAL")
4848
"lib/elixir/pages/getting-started/erlang-libraries.md",
4949
"lib/elixir/pages/getting-started/debugging.md",
5050
"lib/elixir/pages/cheatsheets/enum-cheat.cheatmd",
51+
"lib/elixir/pages/cheatsheets/types-cheat.cheatmd",
5152
"lib/elixir/pages/anti-patterns/what-anti-patterns.md",
5253
"lib/elixir/pages/anti-patterns/code-anti-patterns.md",
5354
"lib/elixir/pages/anti-patterns/design-anti-patterns.md",

lib/elixir/test/elixir/module/types/expr_test.exs

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,36 @@ defmodule Module.Types.ExprTest do
10581058
# from: types_test.ex:LINE-3
10591059
x = %{foo: :baz}
10601060
"""
1061+
1062+
# The goal of this assertion is to verify we assert keys,
1063+
# even if they may be overridden later.
1064+
assert typeerror!(
1065+
[key],
1066+
(
1067+
x = %{key: :value}
1068+
%{x | :foo => :baz, key => :bat}
1069+
)
1070+
) == ~l"""
1071+
expected a map with key :foo in map update syntax:
1072+
1073+
%{x | :foo => :baz, key => :bat}
1074+
1075+
but got type:
1076+
1077+
%{key: :value}
1078+
1079+
where "key" was given the type:
1080+
1081+
# type: dynamic()
1082+
# from: types_test.ex:LINE-5
1083+
key
1084+
1085+
where "x" was given the type:
1086+
1087+
# type: %{key: :value}
1088+
# from: types_test.ex:LINE-3
1089+
x = %{key: :value}
1090+
"""
10611091
end
10621092

10631093
test "updating structs" do
@@ -1159,34 +1189,60 @@ defmodule Module.Types.ExprTest do
11591189
)
11601190
) == closed_map(foo: atom([:new]), baz: atom([:old, :bat]))
11611191

1162-
# The goal of this assertion is to verify we assert keys,
1163-
# even if they may be overridden later.
11641192
assert typeerror!(
11651193
[key],
11661194
(
1167-
x = %{key: :value}
1168-
%{x | :foo => :baz, key => :bat}
1195+
x = %{String.to_integer(key) => :old}
1196+
%{x | String.to_atom(key) => :new}
11691197
)
11701198
) == ~l"""
1171-
expected a map with key :foo in map update syntax:
1199+
expected a map with key of type atom() in map update syntax:
11721200
1173-
%{x | :foo => :baz, key => :bat}
1201+
%{x | String.to_atom(key) => :new}
11741202
11751203
but got type:
11761204
1177-
%{key: :value}
1205+
%{integer() => if_set(:old)}
11781206
11791207
where "key" was given the type:
11801208
1181-
# type: dynamic()
1182-
# from: types_test.ex:LINE-5
1183-
key
1209+
# type: binary()
1210+
# from: types_test.ex:LINE-3
1211+
String.to_integer(key)
11841212
11851213
where "x" was given the type:
11861214
1187-
# type: %{key: :value}
1215+
# type: %{integer() => if_set(:old)}
11881216
# from: types_test.ex:LINE-3
1189-
x = %{key: :value}
1217+
x = %{String.to_integer(key) => :old}
1218+
"""
1219+
1220+
assert typeerror!(
1221+
[key],
1222+
(
1223+
x = %{key: :old}
1224+
%{x | String.to_atom(key) => :new}
1225+
)
1226+
) == ~l"""
1227+
expected a map with key of type atom() in map update syntax:
1228+
1229+
%{x | String.to_atom(key) => :new}
1230+
1231+
but got type:
1232+
1233+
%{key: :old}
1234+
1235+
where "key" was given the type:
1236+
1237+
# type: binary()
1238+
# from: types_test.ex:LINE-2
1239+
String.to_atom(key)
1240+
1241+
where "x" was given the type:
1242+
1243+
# type: %{key: :old}
1244+
# from: types_test.ex:LINE-3
1245+
x = %{key: :old}
11901246
"""
11911247
end
11921248

0 commit comments

Comments
 (0)