[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