Different views for same erlang record

Viewed 82

Suppose I have a record

-record(expense, {uuid, amount, tags}).

I would like to have a displayable version of the record. Tags field contains tag unique ids. I would like to display the name of tags on a form instead of unique ids. How would you do it in Erlang ? Usually, in a OOP language, you would do a ViewModel to have different displayable version of the same object.

Options 1 Use the same record with different data format but I think it would break the interface contract ; one will not be able to know which version of the record one have.

Option 2 Create another record

-record(expense_view1, {uuid, amount, tags}).

But it would create a lot of duplicated records.

Option 3 Use a tuple or map. Tuple is difficult to maintain if I add more fields to the record, and maps doesn't garantee the safety of field names.

2 Answers

Tags field contains tag unique ids. I would like to display the name of tags on a form instead of unique ids.

How about this:

-module(a).
-compile(export_all).

-record(expense, {uuid, amount, tags}).

show_action(#expense{uuid=UUID, amount=Amount, tags={A, B, C} }) ->
    TagConversions= #{1 => "Joe", 2 => "Tammy", 3 => "Bob"},
    A_Conv = maps:get(A, TagConversions, "Nathan"),
    B_Conv = maps:get(B, TagConversions, "Nathan"),
    C_Conv = maps:get(C, TagConversions, "Nathan"),
    io:format("~w, ~w, {~s,~s,~s}~n", 
              [UUID, Amount, A_Conv, B_Conv, C_Conv]).


go() ->
    Expense1 = #expense{uuid=1, amount=10, tags={1,2,3} },
    show_action(Expense1).

In the shell:

12> c(a).  
a.erl:2: Warning: export_all flag enabled - all functions will be exported
{ok,a}

13> a:go().
1, 10, {Joe,Tammy,Bob}
ok

I would like to have a displayable version of the record. But it would create a lot of duplicated records.

Yes, but in OOP don't you have a proliferation of View objects that contain all or a subset of the data in the Model objects?

Options 1 Use the same record with different data format but I think it would break the interface contract ; one will not be able to know which version of the record one have.

You could structure the record to leave a blank spot for the tag names corresponding to the tag ids, then fill in the tag names when you are able:

-module(a).
-compile(export_all).

-record(tag, {id, name=""}).
-record(expense, {uuid, amount, tags}).

show_action(Expense = #expense{uuid=UUID, amount=Amount, tags={A, B, C} }) ->
    TagConversions= #{1 => "Joe", 2 => "Tammy", 3 => "Bob"},
    A_Conv = maps:get(A#tag.id, TagConversions, "Nathan"),
    B_Conv = maps:get(B#tag.id, TagConversions, "Nathan"),
    C_Conv = maps:get(C#tag.id, TagConversions, "Nathan"),
    io:format("~w, ~w, {~s,~s,~s}~n", 
              [UUID, Amount, A_Conv, B_Conv, C_Conv]),

    Expense#expense{tags={
                      A#tag{name=A_Conv},
                      B#tag{name=B_Conv},
                      C#tag{name=C_Conv}
                     }}.
go() ->

    Expense1 = #expense{uuid=1, amount=10, 
                        tags={#tag{id=1},
                              #tag{id=2},
                              #tag{id=3} }
                       },

    show_action(Expense1).

In the shell:

5> c(a).
a.erl:2: Warning: export_all flag enabled - all functions will be exported
{ok,a}

6> a:go().
1, 10, {Joe,Tammy,Bob}
{expense,1,10,{{tag,1,"Joe"},{tag,2,"Tammy"},{tag,3,"Bob"}}}

You could choose the view mode in the access function:

-module (tuple).

-export ([get/1,get/2,start/0,stop/0]).


-record(expense, {uuid, amount, tags}).

%%%%%%%%% Interfaces %%%%%%%%%

% start the server with a static map
start() ->
    Pid =spawn(fun() -> loop(#{1 => one, 2 => two, 3 => three}) end),
    register(server, Pid).

stop() ->
    server ! stop.

% By default for "external users" get the view with value
get(T) ->
    get(T,value).

% for "internal usage" it is possible to choose either the id view or the value view
get(T,value) ->
    Values = lists:map(fun get_value/1, T#expense.tags),
    T#expense{tags = Values};
get(T,id) ->
    T.

%%%%%%%%% server %%%%%%%%%%
% the server is in charge to store the id => value association
% it could be also stored in an ETS, a database ...
loop(Ids) ->
    receive
        stop ->
            done;
        {From, get_value, Id} ->
            % the choice is made to do not crash if the id does not exist
            From ! {ok,maps:get(Id, Ids, undefined)},
            loop(Ids)
    end.

%%%%%%%%% private %%%%%%%%%

get_value(Id) ->
    server ! {self(), get_value, Id},
    receive
        {ok,Value} ->
            Value
    end.

which gives in the shell:

1> c(tuple).                                                  
{ok,tuple}
2> rr(tuple).                                                 
[expense]
3> T = #expense{uuid = 12345, amount = 20000, tags = [1,3,4]}.
#expense{uuid = 12345,amount = 20000,tags = [1,3,4]}
4> tuple:start().                                             
true
5> tuple:get(T).                                              
#expense{uuid = 12345,amount = 20000,
         tags = [one,three,undefined]}
6> tuple:get(T,value).
#expense{uuid = 12345,amount = 20000,
         tags = [one,three,undefined]}
7> tuple:get(T,id).   
#expense{uuid = 12345,amount = 20000,tags = [1,3,4]}
8> tuple:stop().
stop
Related