Types in runtime (basic reflection)

I found that I really lack type information in runtime. To give an example:

-type my_map() :: #{
    key => integer(),
    value => string()
}.

-spec save_to_file(Var :: my_map()) -> ok.
save_to_file(Var) ->
    file:write_file("/tmp/file", term_to_binary(Var)).

-spec load_from_file() -> my_map().
load_from_file() ->
    {ok, Bin} = file:read_file("/tmp/file"),
    MaybeMyMap = binary_to_term(Bin),
    ensure_it_is_my_map(MaybeMyMap).

ensure_it_is_my_map(#{key := Key, value := Value} = MyMap) when is_integer(Key), is_list(Value) ->
    MyMap.

The function ensure_my_map has no added value and can be made generic. For this, runtime needs to have some understanding of what my_map type is.

For functions, this has been already implemented in the form of module_info/0,1 calls that return functions, attributes etc… But it does not return user-defined types.

I solved this by adding a parse_transform module that adds type_info/0,1 function, and I’m wondering if there are more opinions on that matter. Or maybe it is a (missing for now) part of module_info/0,1? That should be easy to add - but hard to remove (the usual dilemma).

3 Likes

This is not as strong, but it feels a bit like how the elixir struct works.

I think that tagging the map and possibly defining a “guard” to guard it is probably the easiest way for now. Idk how much of that could be optimised and understood by the compiler though.

I suppose in theory we could also imagine doing this a as a protocol in elixir.

Once again, I know it is not exactly what you are asking for and I am not sure it helps, but i think filling the “currently possible options on the BEAM for adjacent problems” is of value

1 Like

That really doesn’t seem like a natural thing to do
in Erlang. There are, as it were, two type systems
in Erlang. There is the dynamic types of your data:
integer, float, atom, binary, pair, tuple, map, pid,
&c. That’s the ground truth about what a datum IS.
Then there are the static types assigned to functions.
That’s your intentions about how to VIEW your data.
It is perfectly possible for a datum to belong to an
infinite number of static types.

If you want to be able to make finer distinctions at
run time than the dynamic type system can express,
it is YOUR responsibility to tag the data in some
way. This will be a very rare thing to do, so it
would be inappropriate to impose this overhead on
every datum.

Sorry if I didn’t make it clear. I wasn’t talking about associating a term() with some class/type, - that is, indeed, something that a developer can explicitly do.

I was after keeping the -type attributes in the compiled *.beam file. Other attributes (even custom ones!) are actually saved in *.beam file, and accessible via Mod:module_info(attributes), but type attributes are not there at all.

1 Like

Yes, it makes sense that -type information should be available in .beam files.

2 Likes

Quick and dirty runtime reflection.

add(return_type) -> an_int;
add(arg_type) -> [an_int];
add([ ]) -> 0;
add( [H|T] ) -> 
  H + add(T).
1 Like

Types are stored in the doc chunks if you happen to generate them for your modules, for example:

> ht(typechecker).

   typechecker

These types are documented in this module:

  -type env() :: #env{}.

  -type typed_record_field() ::
            {typed_record_field, record_field(), type()}.

  -type record_field_type() :: gradualizer_type:af_record_field_type().

  -type record_field() :: gradualizer_type:af_record_field(expr()).

  -type af_field_name() :: gradualizer_type:af_field_name().

  -type forms() :: [form()].

  ...

Their structure is available via the shell_docs / EEP-48 machinery - in the signature field of the metadata:

15> shell_docs:get_type_doc(typechecker, typed_record_field, 0).
[{{type,typed_record_field,0},
  [{file,"typechecker.erl"},{location,112}],
  [<<"typed_record_field/0">>],
  [{p,[],
      [<<"There is no documentation for ">>,
       <<"typed_record_field/0">>]}],
  #{signature =>
        [{attribute,112,type,
                    {typed_record_field,{type,112,tuple,
                                              [{atom,112,typed_record_field},
                                               {user_type,112,record_field,[]},
                                               {user_type,112,type,[]}]},
                                        []}}]}}]

However, sometimes it’s a rabbit hole:

16> shell_docs:get_type_doc(typechecker, record_field, 0).
[{{type,record_field,0},
  [{file,"typechecker.erl"},{location,110}],
  [<<"record_field/0">>],
  [{p,[],
      [<<"There is no documentation for ">>,
       <<"record_field/0">>]}],
  #{signature =>
        [{attribute,110,type,
                    {record_field,{remote_type,110,
                                               [{atom,110,gradualizer_type},
                                                {atom,110,af_record_field},
                                                [{user_type,110,expr,[]}]]},
                                  []}}]}}]
17> shell_docs:get_type_doc(typechecker, expr, 0).
[{{type,expr,0},
  [{file,"typechecker.erl"},{location,75}],
  [<<"expr/0">>],
  [{p,[],[<<"There is no documentation for ">>,<<"expr/0">>]}],
  #{signature =>
        [{attribute,75,type,
                    {expr,{remote_type,75,
                                       [{atom,75,gradualizer_type},{atom,75,abstract_expr},[]]},
                          []}}]}}]
18>
18> shell_docs:get_type_doc(gradualizer_type, abstract_expr, 0).
[{{type,abstract_expr,0},
  [{file,"gradualizer_type.erl"},{location,48}],
  [<<"abstract_expr/0">>],
  [{p,[],
      [<<"There is no documentation for ">>,
       <<"abstract_expr/0">>]}],
  #{signature =>
        [{attribute,48,type,
                    {abstract_expr,{type,48,union,
                                         [{user_type,48,af_literal,[]},
                                          {user_type,49,af_match,[{user_type,49,abstract_expr,[]}]},
                                          {user_type,50,af_variable,[]},
                                          {user_type,51,af_tuple,[{user_type,51,...}]},
                                          {user_type,52,af_nil,[]},
                                          {user_type,53,af_cons,[{...}]},
                                          {user_type,54,af_bin,[...]},
                                          {user_type,55,af_binary_op,...},
                                          {user_type,56,...},
                                          {user_type,...},
                                          {...}|...]},
                                   []}}]}}]
3 Likes