|
| 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