Skip to content

Commit bed8636

Browse files
VLanvinmeta-codesync[bot]
authored andcommitted
eqWAlizer shortcomings
Summary: Documentation about eqWAlizer's limitations and shortcomings. To be completed. Reviewed By: TD5 Differential Revision: D85899626 fbshipit-source-id: 320152f725c821c626d4a656a7f627d2eb5a8447
1 parent d6d029d commit bed8636

2 files changed

Lines changed: 151 additions & 0 deletions

File tree

docs/reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,6 @@
2020
- [Overloaded specs](./reference/advanced.md#overloaded-specs)
2121
- [Experimental and expert features](./reference/advanced.md#experimental-and-expert-features)
2222

23+
- ### [Shortcomings and limitations](./reference/shortcomings.md)
24+
2325
- ### [Reference of errors](./reference/errors.md)

docs/reference/shortcomings.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Shortcomings and limitations of eqWAlizer
2+
3+
### Summary
4+
5+
- [Custom validation/predicates](#custom-validationpredicates)
6+
- [Filtering](#filtering)
7+
- [Disjunction of many cases/clauses](#disjunction-of-many-casesclauses)
8+
- [Distributivity of unions](#distributivity-of-unions)
9+
- [Complex functions (lists, maps)](#complex-functions-lists-maps)
10+
11+
### Custom validation/predicates
12+
13+
Erlang supports a very specific list of predicates as guards, such as `is_atom/1`, `is_record/2`, etc. For these, eqWAlizer is capable of powerful reasoning about types.
14+
However, custom validation functions, or even custom wrapper functions around these predicates are not currently supported.
15+
16+
For example, the following will not typecheck:
17+
```erlang
18+
-spec is_foo(term()) -> boolean().
19+
is_foo(#foo{}) -> true;
20+
is_foo(_) -> false.
21+
22+
-spec filter_foo([term()]) -> [#foo{}].
23+
filter_foo(L) -> [X || X <- L, is_foo(X)].
24+
```
25+
If possible, use a predicate directly:
26+
```erlang
27+
-spec filter_foo([term()]) -> [#foo{}].
28+
filter_foo(L) -> [X || X <- L, is_record(X, foo)].
29+
```
30+
Or wrap it in a macro for better understanding:
31+
```erlang
32+
-spec filter_foo([term()]) -> [#foo{}].
33+
filter_foo(L) -> [X || X <- L, ?IS_FOO(X)].
34+
```
35+
In case the predicate is too complex and cannot be simply expressed in terms of basic Erlang predicates, then one has to resort to escape mechanisms (unsafe type ascriptions, dynamic types, or `eqwalizer:ignore`s). For example:
36+
```erlang
37+
-include_lib("eqwalizer/include/eqwalizer.hrl").
38+
39+
-spec filter_complex([term()]) -> [complex_type()].
40+
filter_complex(L) ->
41+
[?UNCHECKED_CAST(X, complex_type()) || X <- L, is_complex_type(X)].
42+
```
43+
44+
### Filtering
45+
46+
In the same vein as custom validation and custom predicates, filtering operations are only supported on an ad-hoc basis in eqWAlizer.
47+
48+
For example, the following will not be accepted:
49+
```erlang
50+
-spec filter_undefined([foo() | undefined]) -> [foo()].
51+
filter_undefined(L) ->
52+
lists:filter(fun(V) -> V =/= undefined end, L).
53+
```
54+
In this case, support for lists comprehensions is slightly more powerful than `lists:filter/2` in eqWAlizer, so this function can be rewritten into the following accepted one:
55+
```erlang
56+
-spec filter_undefined([foo() | undefined]) -> [foo()].
57+
filter_undefined(L) ->
58+
[V || V <- L, V =/= undefined].
59+
```
60+
However, for more general cases and until gaps are patched, one has to resort to error suppression mechanisms.
61+
62+
### Disjunction of many cases/clauses
63+
64+
When in the presence of multiple clauses, eqWAlizer is capable of powerful reasoning to detect which cases have been handled by the previous clauses, and which cases remain. This is called [occurrence typing](../reference/narrowing.md#occurrence-typing). For example, the following is accepted because eqWAlizer is capable to deduce that `undefined` has been properly filtered in all cases:
65+
```erlang
66+
-spec occ(
67+
integer() | undefined,
68+
integer() | undefined
69+
) -> {integer(), integer()}.
70+
occ(A1, A2) ->
71+
case {A1, A2} of
72+
{undefined, undefined} ->
73+
{0, 0};
74+
{undefined, _} ->
75+
{0, A2};
76+
{_, undefined} ->
77+
{A1, 0};
78+
_ ->
79+
{A1, A2}
80+
end.
81+
```
82+
83+
However, since this problem is exponential in the general case, occurrence typing is disabled in each of the following situations:
84+
1. If there are more than 5 clauses;
85+
2. If there are too many intricate guards, leading to a blowup of the number of cases;
86+
3. If an already-bound variable is used in a pattern.
87+
88+
For the first two cases, the solution is to allow eqWAlizer to perform arbitrarily expensive computations, by adding the following:
89+
```erlang
90+
-eqwalizer({unlimited_refinement, parent_function/5}).
91+
```
92+
93+
For the last case, the best solution is to not use a bound variable, and instead add an equality check. For example:
94+
```erlang
95+
f(X, Y) ->
96+
case X of
97+
{a, Y} -> ok;
98+
_ -> error
99+
end.
100+
```
101+
should rather be written as:
102+
```erlang
103+
f(X, Y) ->
104+
case X of
105+
{a, Y2} when Y2 =:= Y -> ok;
106+
_ -> error
107+
end.
108+
```
109+
110+
### Distributivity of unions
111+
112+
When one or several members of a tuple are unions, it is, in theory, possible to distribute them. For example, `{a | b, foo()}` can be distributed to the equivalent type `{a, foo()} | {b, foo()}`.
113+
114+
However, in the general case, this distribution is exponential and breaks eqWAlizer's performance. Hence, it is done on a best-effort basis and only in specific cases. This can lead to confusing signal, for example:
115+
```erlang
116+
-spec test_impl([foo()], [boolean()]) -> {[foo()], [foo()]}.
117+
test_impl(Foos, Bars) ->
118+
wa_lists:partitionmap(fun({Foo, Bar}) -> {Bar =:= true, Foo} end, lists:zip(Foos, Bars)).
119+
120+
Expression has type: fun(({foo(), bar()}) -> {boolean(), foo()})
121+
Context expected type: fun(({foo(), bar()}) -> {'true', L} | {'false', R})
122+
```
123+
124+
Because of generic types `L` and `R`, eqWAlizer cannot do the distribution properly. There are several ways to overcome this. One is to use a type ascription to force eqWAlizer to distribute the union:
125+
```erlang
126+
-include_lib("eqwalizer/include/eqwalizer.hrl").
127+
128+
-spec test_impl([foo()], [bar()]) -> {[foo()], [foo()]}.
129+
test_impl(Foos, Bars) ->
130+
wa_lists:partitionmap(fun({Foo, Bar}) ->
131+
?CHECKED_CAST({Bar =:= true, Foo}, {true, foo()} | {false, foo()})
132+
end, lists:zip(Foos, Bars)).
133+
```
134+
135+
A second solution is to make the union explicit using multiple clauses:
136+
```erlang
137+
-spec test_impl([foo()], [bar()]) -> {[foo()], [foo()]}.
138+
test_impl(Foos, Bars) ->
139+
wa_lists:partitionmap(fun
140+
({Foo, true}) -> {true, Foo};
141+
({Foo, _}) -> {false, Foo}
142+
end, lists:zip(Foos, Bars)).
143+
```
144+
145+
### Complex functions (lists, maps)
146+
147+
OTP functions with complex logic are supported on a case-by-case and best-effort basis in eqWAlizer. This includes most of the functions of the `lists` and `maps` modules, such as `lists:filtermap` and `maps:fold`, for example.
148+
149+
While we continuously improve support for these functions, there are always some gaps. Moreover, very large types (such as large map types) can be prohibitively expensive to support properly, hence we have to resort to some approximations. In many cases, the type of a map will be "flattened", e.g., `#{a => foo(), b => bar()}` will be approximated to `#{a | b => foo() | bar()}`, possibly leading to false positives.

0 commit comments

Comments
 (0)