Is casting to any()
a good solution for having Dialyzer accept ETS match patterns?
Dialyzer and match specifications don't play well together, and there doesn't seem to be a standard solution:
- http://erlang.org/pipermail/erlang-questions/2012-June/067581.html
- https://bugs.erlang.org/browse/ERL-892
Here's a complete example of the solution I'm considering. If matcher('_')
on the last line is changed to '_' then Dialyzer complains about a bad record construction, but with the matcher/1
function all seems to be well:
-module(sample).
-record(rec, {field :: number}).
-export([main/1]).
-type matchvar() :: '$1' | '$2' | '$3' | '$4' | '$5' | '$6' | '$7' | '$8' | '$9' | '$10' | '$11' | '$12' | '$13' | '$14' | '$15' | '$16' | '$17' | '$18' | '$19' | '$20' | '$21' | '$22'.
-spec matcher('_' | matchvar()) -> any().
matcher(X) ->
case node() of
'$ will never match' -> binary_to_term(<<>>);
_ -> X
end.
main(_Args) ->
ets:match('my_table', #rec{field = matcher('$1')}, 1).
This works because Dialyzer can't tell statically that the unreachable first clause of matcher/1
is unreachable. Since binary_to_term/1
returns any()
, Dialyzer infers the return type of matcher/1
to be any()
.
Is this trick a good way of keeping Dialyzer happy when working with match specs? By "good," I mean:
- low runtime cost
- few footguns
- there isn't a better (safer, faster, more ergonomic) way
I peeked at the implementation of node()
and think it's just a pointer dereference, so cost should be low. And '$ will never match' will really never match because node()
always returns an atom with an @
in it. But there must be a better way.
There are really two questions here, that I've combined to avoid the X Y Problem:
- Is the technique above a good way to get Dialyzer to treat something as
any()
? - Is getting Dialyzer treat
matcher('_')
asany()
a good solution for working with match specifications?