Skip to content

Commit 5610775

Browse files
committed
Improve pretty printing of types and fix list tail
1 parent f21c13c commit 5610775

File tree

5 files changed

+131
-77
lines changed

5 files changed

+131
-77
lines changed

lib/elixir/lib/module/types/apply.ex

+5-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,11 @@ defmodule Module.Types.Apply do
218218
{:erlang, :trunc, [{[union(integer(), float())], integer()}]},
219219

220220
# TODO: Replace term()/dynamic() by parametric types
221-
{:erlang, :++, [{[list(term()), term()], dynamic(list(term(), term()))}]},
221+
{:erlang, :++,
222+
[
223+
{[empty_list(), term()], dynamic(term())},
224+
{[non_empty_list(term()), term()], dynamic(non_empty_list(term(), term()))}
225+
]},
222226
{:erlang, :--, [{[list(term()), list(term())], dynamic(list(term()))}]},
223227
{:erlang, :andalso, [{[boolean(), term()], dynamic()}]},
224228
{:erlang, :delete_element, [{[integer(), open_tuple([])], dynamic(open_tuple([]))}]},

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

+83-52
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ defmodule Module.Types.Descr do
4040
}
4141
@empty_list %{bitmap: @bit_empty_list}
4242
@not_non_empty_list Map.delete(@term, :list)
43+
@not_list Map.replace!(@not_non_empty_list, :bitmap, @bit_top - @bit_empty_list)
4344

4445
@empty_intersection [0, @none]
4546
@empty_difference [0, []]
@@ -69,7 +70,7 @@ defmodule Module.Types.Descr do
6970
def integer(), do: %{bitmap: @bit_integer}
7071
def float(), do: %{bitmap: @bit_float}
7172
def fun(), do: %{bitmap: @bit_fun}
72-
def list(type, tail \\ @empty_list), do: list_descr(type, tail, true)
73+
def list(type), do: list_descr(type, @empty_list, true)
7374
def non_empty_list(type, tail \\ @empty_list), do: list_descr(type, tail, false)
7475
def open_map(), do: %{map: @map_top}
7576
def open_map(pairs), do: map_descr(:open, pairs)
@@ -391,11 +392,7 @@ defmodule Module.Types.Descr do
391392
case descr do
392393
%{list: list, bitmap: bitmap} when (bitmap &&& @bit_empty_list) != 0 ->
393394
descr = descr |> Map.delete(:list) |> Map.replace!(:bitmap, bitmap - @bit_empty_list)
394-
395-
case list_to_quoted(list, :list, opts) do
396-
[] -> {[{:empty_list, [], []}], descr}
397-
unions -> {unions, descr}
398-
end
395+
{list_to_quoted(list, true, opts), descr}
399396

400397
%{} ->
401398
{[], descr}
@@ -418,7 +415,7 @@ defmodule Module.Types.Descr do
418415
defp to_quoted(:bitmap, val, _opts), do: bitmap_to_quoted(val)
419416
defp to_quoted(:dynamic, descr, opts), do: dynamic_to_quoted(descr, opts)
420417
defp to_quoted(:map, dnf, opts), do: map_to_quoted(dnf, opts)
421-
defp to_quoted(:list, dnf, opts), do: list_to_quoted(dnf, :non_empty_list, opts)
418+
defp to_quoted(:list, dnf, opts), do: list_to_quoted(dnf, false, opts)
422419
defp to_quoted(:tuple, dnf, opts), do: tuple_to_quoted(dnf, opts)
423420

424421
@doc """
@@ -841,13 +838,17 @@ defmodule Module.Types.Descr do
841838

842839
## List
843840

844-
# Represents both list and improper list simultaneously using a pair {list_type, last_type}.
841+
# Represents both list and improper list simultaneously using a pair
842+
# `{list_type, last_type}`.
845843
#
846-
# For proper lists, the last_type is empty_list().
847-
# In general, list(term(), term()) is interpreted as {term(), term()}
848-
# and not non_empty_list(term(), term()).
844+
# We compute if it is a proper or improper list based if the last_type
845+
# is an empty_list() or a list(). In particular, the last_type is not
846+
# pruned to remove the empty_list() or list(), and therefore it may
847+
# contain the list itself. This is ok because operations like `tl`
848+
# effectively return the list itself plus the union of the tail (and
849+
# if the tail includes the list itself, they are equivalent).
849850
#
850-
# Note: A type being none() is handled separately.
851+
# none() types can be given and, while stored, it means the list type is empty.
851852
defp list_descr(list_type, last_type, empty?) do
852853
{list_dynamic?, list_type} = list_pop_dynamic(list_type)
853854
{last_dynamic?, last_type} = list_pop_dynamic(last_type)
@@ -861,6 +862,10 @@ defmodule Module.Types.Descr do
861862
list_new(list_type, last_type)
862863

863864
{dnf, last_type} ->
865+
# It is safe to discard the negations for the tail because
866+
# `list(term()) and not list(integer())` means a list
867+
# of all terms except lists where all of them are integer,
868+
# which means the head is still a term().
864869
{list_type, last_type} =
865870
Enum.reduce(dnf, {list_type, last_type}, fn {head, tail, _}, {acc_head, acc_tail} ->
866871
{union(head, acc_head), union(tail, acc_tail)}
@@ -1051,55 +1056,77 @@ defmodule Module.Types.Descr do
10511056

10521057
defp list_tl_static(:term), do: :term
10531058

1054-
defp list_tl_static(descr) do
1055-
case descr do
1056-
%{list: dnf} ->
1057-
Enum.reduce(dnf, %{list: dnf, bitmap: @bit_empty_list}, fn {_, last, _}, acc ->
1058-
union(last, acc)
1059-
end)
1059+
defp list_tl_static(%{list: dnf} = descr) do
1060+
initial =
1061+
case descr do
1062+
%{bitmap: bitmap} when (bitmap &&& @bit_empty_list) != 0 ->
1063+
%{list: dnf, bitmap: @bit_empty_list}
10601064

1061-
%{bitmap: bitmap} when (bitmap &&& @bit_empty_list) != 0 ->
1062-
empty_list()
1065+
%{} ->
1066+
%{list: dnf}
1067+
end
10631068

1064-
%{} ->
1065-
none()
1066-
end
1069+
Enum.reduce(dnf, initial, fn {_, last, _}, acc ->
1070+
union(last, acc)
1071+
end)
10671072
end
10681073

1069-
defp list_to_quoted(dnf, name, opts) do
1074+
defp list_tl_static(%{}), do: none()
1075+
1076+
defp list_improper_static?(:term), do: false
1077+
defp list_improper_static?(%{bitmap: bitmap}) when (bitmap &&& @bit_empty_list) != 0, do: false
1078+
defp list_improper_static?(term), do: equal?(term, @not_list)
1079+
1080+
defp list_to_quoted(dnf, empty?, opts) do
10701081
dnf = list_normalize(dnf)
10711082

1072-
for {list_type, last_type, negs} <- dnf, reduce: [] do
1073-
acc ->
1074-
arguments =
1075-
if subtype?(last_type, @empty_list) do
1076-
[to_quoted(list_type, opts)]
1083+
{unions, list_rendered?} =
1084+
Enum.reduce(dnf, {[], false}, fn {list_type, last_type, negs}, {acc, list_rendered?} ->
1085+
{name, arguments, list_rendered?} =
1086+
cond do
1087+
list_type == term() and list_improper_static?(last_type) ->
1088+
{:improper_list, [], list_rendered?}
1089+
1090+
subtype?(last_type, @empty_list) ->
1091+
name = if empty?, do: :list, else: :non_empty_list
1092+
{name, [to_quoted(list_type, opts)], empty?}
1093+
1094+
true ->
1095+
args = [to_quoted(list_type, opts), to_quoted(last_type, opts)]
1096+
{:non_empty_list, args, list_rendered?}
1097+
end
1098+
1099+
acc =
1100+
if negs == [] do
1101+
[{name, [], arguments} | acc]
10771102
else
1078-
[to_quoted(list_type, opts), to_quoted(last_type, opts)]
1103+
negs
1104+
|> Enum.map(fn {ty, lst} ->
1105+
args =
1106+
if subtype?(lst, @empty_list) do
1107+
[to_quoted(ty, opts)]
1108+
else
1109+
[to_quoted(ty, opts), to_quoted(lst, opts)]
1110+
end
1111+
1112+
{name, [], args}
1113+
end)
1114+
|> Enum.reduce(&{:or, [], [&2, &1]})
1115+
|> Kernel.then(
1116+
&[
1117+
{:and, [], [{name, [], arguments}, {:not, [], [&1]}]}
1118+
| acc
1119+
]
1120+
)
10791121
end
10801122

1081-
if negs == [] do
1082-
[{name, [], arguments} | acc]
1083-
else
1084-
negs
1085-
|> Enum.map(fn {ty, lst} ->
1086-
args =
1087-
if subtype?(lst, @empty_list) do
1088-
[to_quoted(ty, opts)]
1089-
else
1090-
[to_quoted(ty, opts), to_quoted(lst, opts)]
1091-
end
1092-
1093-
{name, [], args}
1094-
end)
1095-
|> Enum.reduce(&{:or, [], [&2, &1]})
1096-
|> Kernel.then(
1097-
&[
1098-
{:and, [], [{name, [], arguments}, {:not, [], [&1]}]}
1099-
| acc
1100-
]
1101-
)
1102-
end
1123+
{acc, list_rendered?}
1124+
end)
1125+
1126+
if empty? and not list_rendered? do
1127+
[{:empty_list, [], []} | unions]
1128+
else
1129+
unions
11031130
end
11041131
end
11051132

@@ -1858,6 +1885,10 @@ defmodule Module.Types.Descr do
18581885
{:empty_map, [], []}
18591886
end
18601887

1888+
def map_literal_to_quoted({:open, fields}, _opts) when map_size(fields) == 0 do
1889+
{:map, [], []}
1890+
end
1891+
18611892
def map_literal_to_quoted({:open, %{__struct__: @not_atom_or_optional} = fields}, _opts)
18621893
when map_size(fields) == 1 do
18631894
{:non_struct_map, [], []}

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

+14-6
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Intersections will find the elements in common between the operands. For example
3030

3131
## The syntax of data types
3232

33-
In this section we will cover the syntax of all data types.
33+
In this section we will cover the syntax of all data types. At the moment, developers will interact with those types mostly through compiler warnings and diagnostics.
3434

3535
### Indivisible types
3636

@@ -56,18 +56,26 @@ You can also specify the type of the list element as argument. For example, `lis
5656

5757
Internally, Elixir represents the type `list(a)` as the union two distinct types, `empty_list()` and `not_empty_list(a)`. In other words, `list(integer())` is equivalent to `empty_list() or non_empty_list(integer())`.
5858

59-
Elixir also supports improper lists, where the last element is not an empty list, via `non_empty_list(elem_type, tail_type)`. For example, the value `[1, 2 | 3]` would have the type `non_empty_list(integer(), integer())`.
59+
#### Improper lists
60+
61+
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.
62+
63+
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())`.
64+
65+
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())`.
6066

6167
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:
6268

6369
list() == empty_list() or non_empty_list(term())
64-
improper_list() == non_empty_list(term(), term() and not empty_list())
70+
improper_list() == non_empty_list(term(), term() and not list())
6571

6672
### Maps
6773

6874
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.
6975

70-
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. Structs are closed maps with the `__struct__` key pointing to the struct name.
76+
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.
77+
78+
Structs are closed maps with the `__struct__` key pointing to the struct name.
7179

7280
### Functions
7381

@@ -105,7 +113,7 @@ def negate(x) when is_integer(x), do: -x
105113
def negate(x) when is_boolean(x), do: not x
106114
```
107115

108-
Elixir type checks it as if the function had the type `(dynamic() -> dynamic())`. Then, based on patterns and guards, we can refine the value of the variable `x` to be `dynamic() and integer()` and `dynamic() and boolean()` for each clause respectively. We say `dynamic()` is a gradual type, which leads us to *gradual set-theoretic types*.
116+
Elixir type checks it as if the function had the type `(dynamic() -> dynamic())`. Then, based on patterns and guards, we can refine the value of the variable `x` to be `dynamic() and integer()` and `dynamic() and boolean()` for each clause respectively. We say `dynamic()` is a gradual type, which leads us to _gradual set-theoretic types_.
109117

110118
The simplest way to reason about `dynamic()` in Elixir is that it is a range of types. If you have a type `atom() or integer()`, the underlying code needs to work with both `atom() or integer()`. For example, if you call `Integer.to_string(var)`, and `var` has type `atom() or integer()`, the type system will emit a warning, because `Integer.to_string/1` does not accept atoms.
111119

@@ -131,7 +139,7 @@ Inferring type signatures comes with a series of trade-offs:
131139

132140
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir has a two-steps system, where we first perform module-local inference on functions without type signatures, and then we type check all modules. Module-local inference means the types of the arguments, return values, and all variables are computed considering all of the function calls to the same module and to Elixir's standard library. Any call to a function in another module is conservatively assumed to return `dynamic()` during inference.
133141

134-
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type *will* fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing without requiring any effort from them.
142+
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing without requiring any effort from them.
135143

136144
In the long term, Elixir developers who want typing guarantees must explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages, without performing type inference. In summary, type checking will rely on type signatures and only fallback to inferred types when no signature is available.
137145

0 commit comments

Comments
 (0)