[erlang-questions] Temporarily violating record type constraints annoys dialyzer
Krzysztof Jurewicz
krzysztof.jurewicz@REDACTED
Mon Nov 12 13:49:09 CET 2018
A similar problem arises if we try to use record syntax to construct generators for property testing:
-module(foo_tests).
-include_lib("triq/include/triq.hrl").
-record(
project,
{name :: binary(),
budget :: non_neg_integer(),
successful :: boolean()}).
prop_successful_projects_are_succesful() ->
?FORALL(
Project,
#project{
name=binary(),
budget=non_neg_integer(),
successful=true},
Project#project.successful).
The tests pass, but Dialyzer complains. To silence it, we could rewrite the property as:
prop_successful_projects_are_succesful() ->
?FORALL(
Project,
?LET(
{Name, Budget},
{binary(), non_neg_integer()},
#project{
name=Name,
budget=Budget,
successful=true}),
Project#project.successful).
This is however more verbose.
We may want to put a generator into a function:
project() ->
#project{
name=binary(),
budget=non_neg_integer(),
successful=boolean()}.
The property may then be written as:
prop_successful_projects_are_succesful() ->
?FORALL(
Project,
(project())#project{successful=true},
Project#project.successful).
But again Dialyzer complains. If we wanted to rewrite project() in a ?LET form, then we wouldn’t be able to write (project())#project{successful=true}, as ?LET results in a different data structure.
The root problem here is that by writing #project{name=binary(), budget=non_neg_integer(), successful=boolean()} we don’t really want to create a project record. Instead, we want to create a tuple with a structure resembling the project record which will then be used to create a concrete project record. Dialyzer however doesn’t know about that.
In this particular case one way to silence Dialyzer is to write the project record as:
-record(
project,
{name :: binary() | triq_dom:domain(binary()),
budget :: non_neg_integer() | triq_dom:domain(non_neg_integer()),
successful :: boolean() | triq_dom:domain(boolean())}).
This is however repetitive and also superfluous in production environment.
There is a parse transform named dynarec ( https://github.com/dieswaytoofast/dynarec ) “that automaticaly generates and exports accessors for all records declared within a module”. Theoretically it could be expanded to generate a function named from_map/2 which would look like this:
from_map(
project,
#{name := Name,
budget := Budget,
successful := Successful}) ->
#project{
name=Name,
budget=Budget,
successful=Successful}.
We could then expand the project/0 generator as:
project(FieldMap) ->
?LET(
ProjectMap,
maps:merge(
#{name => binary(),
budget => non_neg_integer(),
successful => bool()},
FieldMap),
from_map(project, ProjectMap)).
It would allow us to leave the project record in its original form and rewrite the property as:
prop_successful_projects_are_succesful() ->
?FORALL(
Project,
project(#{successful => true}),
Project#project.successful).
The parse_widget/1 function from the original post could be written as:
parse_widget(Props) ->
from_map(widget, maps:from_list(Props)).
Generation of from_map/2 is however currently not implemented in dynarec.
More information about the erlang-questions
mailing list