From 872a6b2f679b0371d4be0dc0d607b83187c39208 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Fri, 26 Apr 2019 19:09:19 +0300 Subject: [PATCH 01/18] ignore test files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index c2545e3..0e9bc22 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ rebar3 rebar.lock _build/ *.sublime-* +t.erl + From d412abfec6446150198fe79a6891763d0ce2f098 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Fri, 26 Apr 2019 21:56:21 +0300 Subject: [PATCH 02/18] define generic for product type --- Makefile | 10 +- erlang.mk => beam.mk | 36 +- rebar.config | 1 + src/generic.erl | 810 ++++++++++++++++++++++++++++++++++++++++ test/category_SUITE.erl | 12 +- test/generic_SUITE.erl | 104 ++++++ 6 files changed, 945 insertions(+), 28 deletions(-) rename erlang.mk => beam.mk (90%) create mode 100644 src/generic.erl create mode 100644 test/generic_SUITE.erl diff --git a/Makefile b/Makefile index 9d86e67..d09f913 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,6 @@ -## -## @doc -## an example Makefile to build and ship erlang software -## -## APP - identity of the application -## ORG - identity of the organization -## URI - identity of the docker repository with last / - APP = datum ORG = fogfish URI = -include erlang.mk +include beam.mk diff --git a/erlang.mk b/beam.mk similarity index 90% rename from erlang.mk rename to beam.mk index 7b16564..b18cb28 100644 --- a/erlang.mk +++ b/beam.mk @@ -8,7 +8,7 @@ ## @doc ## This makefile is the wrapper of rebar to build and ship erlang software ## -## @version 1.0.9 +## @version 1.0.12 .PHONY: all compile test unit clean distclean run console mock-up mock-rm benchmark release dist APP := $(strip $(APP)) @@ -27,13 +27,13 @@ REL = ${APP}-${VSN} PKG = ${REL}+${ARCH}.${PLAT} TEST ?= tests COOKIE ?= nocookie -DOCKER ?= fogfish/erlang +DOCKER ?= fogfish/erlang-alpine IID = ${URI}${ORG}/${APP} ## required tools ## - rebar version (no spaces at end) ## - path to basho benchmark -REBAR ?= 3.5.0 +REBAR ?= 3.9.1 BB = ../basho_bench @@ -89,17 +89,22 @@ compile: rebar3 ## ## execute common test and terminate node -test: _build/test.beam - @mkdir -p /tmp/test/${APP} - @erl ${EFLAGS} -noshell -pa _build/ -pa test/ -run test run test/${TEST}.config - @F=`ls /tmp/test/${APP}/ct_run*/all.coverdata | tail -n 1` ;\ - cp $$F /tmp/test/${APP}/ct.coverdata - -_build/test.beam: _build/test.erl - @erlc -o _build $< - -_build/test.erl: - @mkdir -p _build && echo "${BOOT_CT}" > $@ +test: + @./rebar3 ct --config=test/${TEST}.config --cover --verbose + @./rebar3 cover + +# test: _build/test.beam +# @mkdir -p /tmp/test/${APP} +# @erl ${EFLAGS} -noshell -pa _build/ -pa test/ -run test run test/${TEST}.config +# @F=`ls /tmp/test/${APP}/ct_run*/all.coverdata | tail -n 1` ;\ +# cp $$F /tmp/test/${APP}/ct.coverdata +# +# _build/test.beam: _build/test.erl +# @erlc -o _build $< +# +# _build/test.erl: +# @mkdir -p _build && echo "${BOOT_CT}" > $@ +# testclean: @rm -f _build/test.beam @@ -148,6 +153,7 @@ mock-rm: test/mock/docker-compose.yml -@docker-compose -f $< down --rmi all -v --remove-orphans dist-up: docker-compose.yml _build/spawner + @docker-compose build @docker-compose -f $< up dist-rm: docker-compose.yml @@ -191,6 +197,8 @@ endif ## build docker image docker: Dockerfile + git status --porcelain + test -z "`git status --porcelain`" || exit -1 docker build \ --build-arg APP=${APP} \ --build-arg VSN=${VSN} \ diff --git a/rebar.config b/rebar.config index 37d43f1..d44ba1c 100644 --- a/rebar.config +++ b/rebar.config @@ -23,6 +23,7 @@ ,"src/topological.erl" ,"src/traversable.erl" + ,"src/generic.erl" ]}. %% diff --git a/src/generic.erl b/src/generic.erl new file mode 100644 index 0000000..1099c86 --- /dev/null +++ b/src/generic.erl @@ -0,0 +1,810 @@ +%% +%% @doc +%% based on stdlib-3.0/examples/erl_id_trans.erl +-module(generic). + +-export([ + to/2 +, toL/2 +, from/3 +, fromL/3 +, parse_transform/2 +]). + + +-spec to([atom()], tuple()) -> map(). + +to(Fields, Struct) -> + maps:from_list( + [{Key, Value} || + {Key, Value} <- lists:zip( + Fields, + tl(tuple_to_list(Struct)) + ), + Value /= undefined, + Value /= null + ] + ). + +-spec toL([atom()], tuple()) -> map(). + +toL(Fields, Struct) -> + maps:from_list( + [{typecast:s(Key), Value} || + {Key, Value} <- lists:zip( + Fields, + tl(tuple_to_list(Struct)) + ), + Value /= undefined, + Value /= null + ] + ). + +-spec from(atom(), [atom()], map()) -> tuple(). + +from(Type, Fields, Struct) -> + list_to_tuple([Type | + [maps:get(X, Struct, undefined) || X <- Fields]] + ). + +-spec fromL(atom(), [atom()], map()) -> tuple(). + +fromL(Type, Fields, Struct) -> + list_to_tuple([Type | + [maps:get(typecast:s(X), Struct, undefined) || X <- Fields]] + ). + +%%%------------------------------------------------------------------ +%%% +%%% stdlib-3.0/examples/erl_id_trans.erl +%%% +%%%------------------------------------------------------------------ + + +%% An identity transformer of Erlang abstract syntax. + +%% This module only traverses legal Erlang code. This is most noticeable +%% in guards where only a limited number of expressions are allowed. +%% N.B. if this module is to be used as a basis for transforms then +%% all the error cases must be handled otherwise this module just crashes! + +parse_transform(Forms, _Options) -> + forms(Forms). + +%% forms(Fs) -> lists:map(fun (F) -> form(F) end, Fs). + +forms([F0|Fs0]) -> + F1 = form(F0), + Fs1 = forms(Fs0), + [F1|Fs1]; +forms([]) -> []. + +%% -type form(Form) -> Form. +%% Here we show every known form and valid internal structure. We do not +%% that the ordering is correct! + +%% First the various attributes. +form({attribute,Line,module,Mod}) -> + {attribute,Line,module,Mod}; +form({attribute,Line,file,{File,Line}}) -> %This is valid anywhere. + {attribute,Line,file,{File,Line}}; +form({attribute,Line,export,Es0}) -> + Es1 = farity_list(Es0), + {attribute,Line,export,Es1}; +form({attribute,Line,import,{Mod,Is0}}) -> + Is1 = farity_list(Is0), + {attribute,Line,import,{Mod,Is1}}; +form({attribute,Line,export_type,Es0}) -> + Es1 = farity_list(Es0), + {attribute,Line,export_type,Es1}; +form({attribute,Line,optional_callbacks,Es0}) -> + try farity_list(Es0) of + Es1 -> + {attribute,Line,optional_callbacks,Es1} + catch + _:_ -> + {attribute,Line,optional_callbacks,Es0} + end; +form({attribute,Line,compile,C}) -> + {attribute,Line,compile,C}; +form({attribute,Line,record,{Name,Defs0}}) -> + Defs1 = record_defs(Defs0), + {attribute,Line,record,{Name,Defs1}}; +form({attribute,Line,asm,{function,N,A,Code}}) -> + {attribute,Line,asm,{function,N,A,Code}}; +form({attribute,Line,type,{N,T,Vs}}) -> + T1 = type(T), + Vs1 = variable_list(Vs), + {attribute,Line,type,{N,T1,Vs1}}; +form({attribute,Line,opaque,{N,T,Vs}}) -> + T1 = type(T), + Vs1 = variable_list(Vs), + {attribute,Line,opaque,{N,T1,Vs1}}; +form({attribute,Line,spec,{{N,A},FTs}}) -> + FTs1 = function_type_list(FTs), + {attribute,Line,spec,{{N,A},FTs1}}; +form({attribute,Line,spec,{{M,N,A},FTs}}) -> + FTs1 = function_type_list(FTs), + {attribute,Line,spec,{{M,N,A},FTs1}}; +form({attribute,Line,callback,{{N,A},FTs}}) -> + FTs1 = function_type_list(FTs), + {attribute,Line,callback,{{N,A},FTs1}}; +form({attribute,Line,Attr,Val}) -> %The general attribute. + {attribute,Line,Attr,Val}; +form({function,Line,Name0,Arity0,Clauses0}) -> + {Name,Arity,Clauses} = function(Name0, Arity0, Clauses0), + {function,Line,Name,Arity,Clauses}; +%% Extra forms from the parser. +form({error,E}) -> {error,E}; +form({warning,W}) -> {warning,W}; +form({eof,Line}) -> {eof,Line}. + +%% -type farity_list([Farity]) -> [Farity] when Farity <= {atom(),integer()}. + +farity_list([{Name,Arity}|Fas]) -> + [{Name,Arity}|farity_list(Fas)]; +farity_list([]) -> []. + +%% -type variable_list([Var]) -> [Var] + +variable_list([{var,Line,Var}|Vs]) -> + [{var,Line,Var}|variable_list(Vs)]; +variable_list([]) -> []. + +%% -type record_defs([RecDef]) -> [RecDef]. +%% N.B. Field names are full expressions here but only atoms are allowed +%% by the *parser*! + +record_defs([{record_field,Line,{atom,La,A},Val0}|Is]) -> + Val1 = expr(Val0), + [{record_field,Line,{atom,La,A},Val1}|record_defs(Is)]; +record_defs([{record_field,Line,{atom,La,A}}|Is]) -> + [{record_field,Line,{atom,La,A}}|record_defs(Is)]; +record_defs([{typed_record_field,{record_field,Line,{atom,La,A},Val0},Type}| + Is]) -> + Val1 = expr(Val0), + Type1 = type(Type), + [{typed_record_field,{record_field,Line,{atom,La,A},Val1},Type1}| + record_defs(Is)]; +record_defs([{typed_record_field,{record_field,Line,{atom,La,A}},Type}|Is]) -> + Type1 = type(Type), + [{typed_record_field,{record_field,Line,{atom,La,A}},Type1}| + record_defs(Is)]; +record_defs([]) -> []. + +%% -type function(atom(), integer(), [Clause]) -> {atom(),integer(),[Clause]}. + +function(Name, Arity, Clauses0) -> + Clauses1 = clauses(Clauses0), + {Name,Arity,Clauses1}. + +%% -type clauses([Clause]) -> [Clause]. + +clauses([C0|Cs]) -> + C1 = clause(C0), + [C1|clauses(Cs)]; +clauses([]) -> []. + +%% -type clause(Clause) -> Clause. + +clause({clause,Line,H0,G0,B0}) -> + H1 = head(H0), + G1 = guard(G0), + B1 = exprs(B0), + {clause,Line,H1,G1,B1}. + +%% -type head([Pattern]) -> [Pattern]. + +head(Ps) -> patterns(Ps). + +%% -type patterns([Pattern]) -> [Pattern]. +%% These patterns are processed "sequentially" for purposes of variable +%% definition etc. + +patterns([P0|Ps]) -> + P1 = pattern(P0), + [P1|patterns(Ps)]; +patterns([]) -> []. + +%% -type pattern(Pattern) -> Pattern. +%% N.B. Only valid patterns are included here. + +pattern({var,Line,V}) -> {var,Line,V}; +pattern({match,Line,L0,R0}) -> + L1 = pattern(L0), + R1 = pattern(R0), + {match,Line,L1,R1}; +pattern({integer,Line,I}) -> {integer,Line,I}; +pattern({char,Line,C}) -> {char,Line,C}; +pattern({float,Line,F}) -> {float,Line,F}; +pattern({atom,Line,A}) -> {atom,Line,A}; +pattern({string,Line,S}) -> {string,Line,S}; +pattern({nil,Line}) -> {nil,Line}; +pattern({cons,Line,H0,T0}) -> + H1 = pattern(H0), + T1 = pattern(T0), + {cons,Line,H1,T1}; +pattern({tuple,Line,Ps0}) -> + Ps1 = pattern_list(Ps0), + {tuple,Line,Ps1}; +pattern({map,Line,Ps0}) -> + Ps1 = pattern_list(Ps0), + {map,Line,Ps1}; +pattern({map_field_exact,Line,K,V}) -> + Ke = expr(K), + Ve = pattern(V), + {map_field_exact,Line,Ke,Ve}; +%%pattern({struct,Line,Tag,Ps0}) -> +%% Ps1 = pattern_list(Ps0), +%% {struct,Line,Tag,Ps1}; +pattern({record,Line,Name,Pfs0}) -> + Pfs1 = pattern_fields(Pfs0), + {record,Line,Name,Pfs1}; +pattern({record_index,Line,Name,Field0}) -> + Field1 = pattern(Field0), + {record_index,Line,Name,Field1}; +pattern({record_field,Line,Rec0,Name,Field0}) -> + Rec1 = expr(Rec0), + Field1 = expr(Field0), + {record_field,Line,Rec1,Name,Field1}; +pattern({record_field,Line,Rec0,Field0}) -> + Rec1 = expr(Rec0), + Field1 = expr(Field0), + {record_field,Line,Rec1,Field1}; +pattern({bin,Line,Fs}) -> + Fs2 = pattern_grp(Fs), + {bin,Line,Fs2}; +pattern({op,Line,Op,A}) -> + {op,Line,Op,A}; +pattern({op,Line,Op,L,R}) -> + {op,Line,Op,L,R}. + +pattern_grp([{bin_element,L1,E1,S1,T1} | Fs]) -> + S2 = case S1 of + default -> + default; + _ -> + expr(S1) + end, + T2 = case T1 of + default -> + default; + _ -> + bit_types(T1) + end, + [{bin_element,L1,expr(E1),S2,T2} | pattern_grp(Fs)]; +pattern_grp([]) -> + []. + +bit_types([]) -> + []; +bit_types([Atom | Rest]) when is_atom(Atom) -> + [Atom | bit_types(Rest)]; +bit_types([{Atom, Integer} | Rest]) when is_atom(Atom), is_integer(Integer) -> + [{Atom, Integer} | bit_types(Rest)]. + + + +%% -type pattern_list([Pattern]) -> [Pattern]. +%% These patterns are processed "in parallel" for purposes of variable +%% definition etc. + +pattern_list([P0|Ps]) -> + P1 = pattern(P0), + [P1|pattern_list(Ps)]; +pattern_list([]) -> []. + +%% -type pattern_fields([Field]) -> [Field]. +%% N.B. Field names are full expressions here but only atoms are allowed +%% by the *linter*!. + +pattern_fields([{record_field,Lf,{atom,La,F},P0}|Pfs]) -> + P1 = pattern(P0), + [{record_field,Lf,{atom,La,F},P1}|pattern_fields(Pfs)]; +pattern_fields([{record_field,Lf,{var,La,'_'},P0}|Pfs]) -> + P1 = pattern(P0), + [{record_field,Lf,{var,La,'_'},P1}|pattern_fields(Pfs)]; +pattern_fields([]) -> []. + +%% -type guard([GuardTest]) -> [GuardTest]. + +guard([G0|Gs]) when is_list(G0) -> + [guard0(G0) | guard(Gs)]; +guard(L) -> + guard0(L). + +guard0([G0|Gs]) -> + G1 = guard_test(G0), + [G1|guard0(Gs)]; +guard0([]) -> []. + +guard_test(Expr={call,Line,{atom,La,F},As0}) -> + case erl_internal:type_test(F, length(As0)) of + true -> + As1 = gexpr_list(As0), + {call,Line,{atom,La,F},As1}; + _ -> + gexpr(Expr) + end; +guard_test(Any) -> + gexpr(Any). + +%% Before R9, there were special rules regarding the expressions on +%% top level in guards. Those limitations are now lifted - therefore +%% there is no need for a special clause for the toplevel expressions. +%% -type gexpr(GuardExpr) -> GuardExpr. + +gexpr({var,Line,V}) -> {var,Line,V}; +gexpr({integer,Line,I}) -> {integer,Line,I}; +gexpr({char,Line,C}) -> {char,Line,C}; +gexpr({float,Line,F}) -> {float,Line,F}; +gexpr({atom,Line,A}) -> {atom,Line,A}; +gexpr({string,Line,S}) -> {string,Line,S}; +gexpr({nil,Line}) -> {nil,Line}; +gexpr({map,Line,Map0,Es0}) -> + [Map1|Es1] = gexpr_list([Map0|Es0]), + {map,Line,Map1,Es1}; +gexpr({map,Line,Es0}) -> + Es1 = gexpr_list(Es0), + {map,Line,Es1}; +gexpr({map_field_assoc,Line,K,V}) -> + Ke = gexpr(K), + Ve = gexpr(V), + {map_field_assoc,Line,Ke,Ve}; +gexpr({map_field_exact,Line,K,V}) -> + Ke = gexpr(K), + Ve = gexpr(V), + {map_field_exact,Line,Ke,Ve}; +gexpr({cons,Line,H0,T0}) -> + H1 = gexpr(H0), + T1 = gexpr(T0), %They see the same variables + {cons,Line,H1,T1}; +gexpr({tuple,Line,Es0}) -> + Es1 = gexpr_list(Es0), + {tuple,Line,Es1}; +gexpr({record_index,Line,Name,Field0}) -> + Field1 = gexpr(Field0), + {record_index,Line,Name,Field1}; +gexpr({record_field,Line,Rec0,Name,Field0}) -> + Rec1 = gexpr(Rec0), + Field1 = gexpr(Field0), + {record_field,Line,Rec1,Name,Field1}; +gexpr({record,Line,Name,Inits0}) -> + Inits1 = grecord_inits(Inits0), + {record,Line,Name,Inits1}; +gexpr({call,Line,{atom,La,F},As0}) -> + case erl_internal:guard_bif(F, length(As0)) of + true -> As1 = gexpr_list(As0), + {call,Line,{atom,La,F},As1} + end; +% Guard bif's can be remote, but only in the module erlang... +gexpr({call,Line,{remote,La,{atom,Lb,erlang},{atom,Lc,F}},As0}) -> + case erl_internal:guard_bif(F, length(As0)) or + erl_internal:arith_op(F, length(As0)) or + erl_internal:comp_op(F, length(As0)) or + erl_internal:bool_op(F, length(As0)) of + true -> As1 = gexpr_list(As0), + {call,Line,{remote,La,{atom,Lb,erlang},{atom,Lc,F}},As1} + end; +gexpr({bin,Line,Fs}) -> + Fs2 = pattern_grp(Fs), + {bin,Line,Fs2}; +gexpr({op,Line,Op,A0}) -> + case erl_internal:arith_op(Op, 1) or + erl_internal:bool_op(Op, 1) of + true -> A1 = gexpr(A0), + {op,Line,Op,A1} + end; +gexpr({op,Line,Op,L0,R0}) when Op =:= 'andalso'; Op =:= 'orelse' -> + %% R11B: andalso/orelse are now allowed in guards. + L1 = gexpr(L0), + R1 = gexpr(R0), %They see the same variables + {op,Line,Op,L1,R1}; +gexpr({op,Line,Op,L0,R0}) -> + case erl_internal:arith_op(Op, 2) or + erl_internal:bool_op(Op, 2) or + erl_internal:comp_op(Op, 2) of + true -> + L1 = gexpr(L0), + R1 = gexpr(R0), %They see the same variables + {op,Line,Op,L1,R1} + end. + +%% -type gexpr_list([GuardExpr]) -> [GuardExpr]. +%% These expressions are processed "in parallel" for purposes of variable +%% definition etc. + +gexpr_list([E0|Es]) -> + E1 = gexpr(E0), + [E1|gexpr_list(Es)]; +gexpr_list([]) -> []. + +grecord_inits([{record_field,Lf,{atom,La,F},Val0}|Is]) -> + Val1 = gexpr(Val0), + [{record_field,Lf,{atom,La,F},Val1}|grecord_inits(Is)]; +grecord_inits([{record_field,Lf,{var,La,'_'},Val0}|Is]) -> + Val1 = gexpr(Val0), + [{record_field,Lf,{var,La,'_'},Val1}|grecord_inits(Is)]; +grecord_inits([]) -> []. + +%% -type exprs([Expression]) -> [Expression]. +%% These expressions are processed "sequentially" for purposes of variable +%% definition etc. + +exprs([E0|Es]) -> + E1 = expr(E0), + [E1|exprs(Es)]; +exprs([]) -> []. + +%% -type expr(Expression) -> Expression. + +expr({var,Line,V}) -> {var,Line,V}; +expr({integer,Line,I}) -> {integer,Line,I}; +expr({float,Line,F}) -> {float,Line,F}; +expr({atom,Line,A}) -> {atom,Line,A}; +expr({string,Line,S}) -> {string,Line,S}; +expr({char,Line,C}) -> {char,Line,C}; +expr({nil,Line}) -> {nil,Line}; +expr({cons,Line,H0,T0}) -> + H1 = expr(H0), + T1 = expr(T0), %They see the same variables + {cons,Line,H1,T1}; +expr({lc,Line,E0,Qs0}) -> + Qs1 = lc_bc_quals(Qs0), + E1 = expr(E0), + {lc,Line,E1,Qs1}; +expr({bc,Line,E0,Qs0}) -> + Qs1 = lc_bc_quals(Qs0), + E1 = expr(E0), + {bc,Line,E1,Qs1}; +expr({tuple,Line,Es0}) -> + Es1 = expr_list(Es0), + {tuple,Line,Es1}; +expr({map,Line,Map0,Es0}) -> + [Map1|Es1] = exprs([Map0|Es0]), + {map,Line,Map1,Es1}; +expr({map,Line,Es0}) -> + Es1 = exprs(Es0), + {map,Line,Es1}; +expr({map_field_assoc,Line,K,V}) -> + Ke = expr(K), + Ve = expr(V), + {map_field_assoc,Line,Ke,Ve}; +expr({map_field_exact,Line,K,V}) -> + Ke = expr(K), + Ve = expr(V), + {map_field_exact,Line,Ke,Ve}; +%%expr({struct,Line,Tag,Es0}) -> +%% Es1 = pattern_list(Es0), +%% {struct,Line,Tag,Es1}; +expr({record_index,Line,Name,Field0}) -> + Field1 = expr(Field0), + {record_index,Line,Name,Field1}; +expr({record,Line,Name,Inits0}) -> + Inits1 = record_inits(Inits0), + {record,Line,Name,Inits1}; +expr({record_field,Line,Rec0,Name,Field0}) -> + Rec1 = expr(Rec0), + Field1 = expr(Field0), + {record_field,Line,Rec1,Name,Field1}; +expr({record,Line,Rec0,Name,Upds0}) -> + Rec1 = expr(Rec0), + Upds1 = record_updates(Upds0), + {record,Line,Rec1,Name,Upds1}; +expr({record_field,Line,Rec0,Field0}) -> + Rec1 = expr(Rec0), + Field1 = expr(Field0), + {record_field,Line,Rec1,Field1}; +expr({block,Line,Es0}) -> + %% Unfold block into a sequence. + Es1 = exprs(Es0), + {block,Line,Es1}; +expr({'if',Line,Cs0}) -> + Cs1 = icr_clauses(Cs0), + {'if',Line,Cs1}; +expr({'case',Line,E0,Cs0}) -> + E1 = expr(E0), + Cs1 = icr_clauses(Cs0), + {'case',Line,E1,Cs1}; +expr({'receive',Line,Cs0}) -> + Cs1 = icr_clauses(Cs0), + {'receive',Line,Cs1}; +expr({'receive',Line,Cs0,To0,ToEs0}) -> + To1 = expr(To0), + ToEs1 = exprs(ToEs0), + Cs1 = icr_clauses(Cs0), + {'receive',Line,Cs1,To1,ToEs1}; +expr({'try',Line,Es0,Scs0,Ccs0,As0}) -> + Es1 = exprs(Es0), + Scs1 = icr_clauses(Scs0), + Ccs1 = icr_clauses(Ccs0), + As1 = exprs(As0), + {'try',Line,Es1,Scs1,Ccs1,As1}; +expr({'fun',Line,Body}) -> + case Body of + {clauses,Cs0} -> + Cs1 = fun_clauses(Cs0), + {'fun',Line,{clauses,Cs1}}; + {function,F,A} -> + {'fun',Line,{function,F,A}}; + {function,M,F,A} when is_atom(M), is_atom(F), is_integer(A) -> + %% R10B-6: fun M:F/A. (Backward compatibility) + {'fun',Line,{function,M,F,A}}; + {function,M0,F0,A0} -> + %% R15: fun M:F/A with variables. + M = expr(M0), + F = expr(F0), + A = expr(A0), + {'fun',Line,{function,M,F,A}} + end; +expr({named_fun,Loc,Name,Cs}) -> + {named_fun,Loc,Name,fun_clauses(Cs)}; + +%% +%% +expr({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, to}}, + [{record, _, {var, _, _} = Struct, Type, _}] +}) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, to}}, + [ + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + expr(Struct) + ] + }; + +expr({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, to}}, + [{record, _, Type, _} = Struct] +}) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, to}}, + [ + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + expr(Struct) + ] + }; + +%% +%% +expr({call, Ln, + {remote, _, {atom, _, genericL}, {atom, _, to}}, + [{record, _, {var, _, _} = Struct, Type, _}] +}) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, toL}}, + [ + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + expr(Struct) + ] + }; + +expr({call, Ln, + {remote, _, {atom, _, genericL}, {atom, _, to}}, + [{record, _, Type, _} = Struct] +}) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, toL}}, + [ + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + expr(Struct) + ] + }; + +%% +%% +expr({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, Type}}, + [Struct] +}) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, from}}, + [ + {atom, Ln, Type}, + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + expr(Struct) + ] + }; + +expr({call, Ln, + {remote, _, {atom, _, genericL}, {atom, _, Type}}, + [Struct] +}) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, fromL}}, + [ + {atom, Ln, Type}, + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + expr(Struct) + ] + }; + +expr({call,Line,F0,As0}) -> + %% N.B. If F an atom then call to local function or BIF, if F a + %% remote structure (see below) then call to other module, + %% otherwise apply to "function". + F1 = expr(F0), + As1 = expr_list(As0), + {call,Line,F1,As1}; +expr({'catch',Line,E0}) -> + %% No new variables added. + E1 = expr(E0), + {'catch',Line,E1}; +expr({match,Line,P0,E0}) -> + E1 = expr(E0), + P1 = pattern(P0), + {match,Line,P1,E1}; +expr({bin,Line,Fs}) -> + Fs2 = pattern_grp(Fs), + {bin,Line,Fs2}; +expr({op,Line,Op,A0}) -> + A1 = expr(A0), + {op,Line,Op,A1}; +expr({op,Line,Op,L0,R0}) -> + L1 = expr(L0), + R1 = expr(R0), %They see the same variables + {op,Line,Op,L1,R1}; +%% The following are not allowed to occur anywhere! +expr({remote,Line,M0,F0}) -> + M1 = expr(M0), + F1 = expr(F0), + {remote,Line,M1,F1}. + +%% -type expr_list([Expression]) -> [Expression]. +%% These expressions are processed "in parallel" for purposes of variable +%% definition etc. + +expr_list([E0|Es]) -> + E1 = expr(E0), + [E1|expr_list(Es)]; +expr_list([]) -> []. + +%% -type record_inits([RecordInit]) -> [RecordInit]. +%% N.B. Field names are full expressions here but only atoms are allowed +%% by the *linter*!. + +record_inits([{record_field,Lf,{atom,La,F},Val0}|Is]) -> + Val1 = expr(Val0), + [{record_field,Lf,{atom,La,F},Val1}|record_inits(Is)]; +record_inits([{record_field,Lf,{var,La,'_'},Val0}|Is]) -> + Val1 = expr(Val0), + [{record_field,Lf,{var,La,'_'},Val1}|record_inits(Is)]; +record_inits([]) -> []. + +%% -type record_updates([RecordUpd]) -> [RecordUpd]. +%% N.B. Field names are full expressions here but only atoms are allowed +%% by the *linter*!. + +record_updates([{record_field,Lf,{atom,La,F},Val0}|Us]) -> + Val1 = expr(Val0), + [{record_field,Lf,{atom,La,F},Val1}|record_updates(Us)]; +record_updates([]) -> []. + +%% -type icr_clauses([Clause]) -> [Clause]. + +icr_clauses([C0|Cs]) -> + C1 = clause(C0), + [C1|icr_clauses(Cs)]; +icr_clauses([]) -> []. + +%% -type lc_bc_quals([Qualifier]) -> [Qualifier]. +%% Allow filters to be both guard tests and general expressions. + +lc_bc_quals([{generate,Line,P0,E0}|Qs]) -> + E1 = expr(E0), + P1 = pattern(P0), + [{generate,Line,P1,E1}|lc_bc_quals(Qs)]; +lc_bc_quals([{b_generate,Line,P0,E0}|Qs]) -> + E1 = expr(E0), + P1 = pattern(P0), + [{b_generate,Line,P1,E1}|lc_bc_quals(Qs)]; +lc_bc_quals([E0|Qs]) -> + E1 = expr(E0), + [E1|lc_bc_quals(Qs)]; +lc_bc_quals([]) -> []. + +%% -type fun_clauses([Clause]) -> [Clause]. + +fun_clauses([C0|Cs]) -> + C1 = clause(C0), + [C1|fun_clauses(Cs)]; +fun_clauses([]) -> []. + +function_type_list([{type,Line,bounded_fun,[Ft,Fc]}|Fts]) -> + Ft1 = function_type(Ft), + Fc1 = function_constraint(Fc), + [{type,Line,bounded_fun,[Ft1,Fc1]}|function_type_list(Fts)]; +function_type_list([Ft|Fts]) -> + [function_type(Ft)|function_type_list(Fts)]; +function_type_list([]) -> []. + +function_type({type,Line,'fun',[{type,Lt,product,As},B]}) -> + As1 = type_list(As), + B1 = type(B), + {type,Line,'fun',[{type,Lt,product,As1},B1]}. + +function_constraint([C|Cs]) -> + C1 = constraint(C), + [C1|function_constraint(Cs)]; +function_constraint([]) -> []. + +constraint({type,Line,constraint,[{atom,L,A},[V,T]]}) -> + V1 = type(V), + T1 = type(T), + {type,Line,constraint,[{atom,L,A},[V1,T1]]}. + +type({ann_type,Line,[{var,Lv,V},T]}) -> + T1 = type(T), + {ann_type,Line,[{var,Lv,V},T1]}; +type({atom,Line,A}) -> + {atom,Line,A}; +type({integer,Line,I}) -> + {integer,Line,I}; +type({op,Line,Op,T}) -> + T1 = type(T), + {op,Line,Op,T1}; +type({op,Line,Op,L,R}) -> + L1 = type(L), + R1 = type(R), + {op,Line,Op,L1,R1}; +type({type,Line,binary,[M,N]}) -> + M1 = type(M), + N1 = type(N), + {type,Line,binary,[M1,N1]}; +type({type,Line,'fun',[]}) -> + {type,Line,'fun',[]}; +type({type,Line,'fun',[{type,Lt,any},B]}) -> + B1 = type(B), + {type,Line,'fun',[{type,Lt,any},B1]}; +type({type,Line,range,[L,H]}) -> + L1 = type(L), + H1 = type(H), + {type,Line,range,[L1,H1]}; +type({type,Line,map,any}) -> + {type,Line,map,any}; +type({type,Line,map,Ps}) -> + Ps1 = map_pair_types(Ps), + {type,Line,map,Ps1}; +type({type,Line,record,[{atom,La,N}|Fs]}) -> + Fs1 = field_types(Fs), + {type,Line,record,[{atom,La,N}|Fs1]}; +type({remote_type,Line,[{atom,Lm,M},{atom,Ln,N},As]}) -> + As1 = type_list(As), + {remote_type,Line,[{atom,Lm,M},{atom,Ln,N},As1]}; +type({type,Line,tuple,any}) -> + {type,Line,tuple,any}; +type({type,Line,tuple,Ts}) -> + Ts1 = type_list(Ts), + {type,Line,tuple,Ts1}; +type({type,Line,union,Ts}) -> + Ts1 = type_list(Ts), + {type,Line,union,Ts1}; +type({var,Line,V}) -> + {var,Line,V}; +type({user_type,Line,N,As}) -> + As1 = type_list(As), + {user_type,Line,N,As1}; +type({type,Line,N,As}) -> + As1 = type_list(As), + {type,Line,N,As1}. + +map_pair_types([{type,Line,map_field_assoc,[K,V]}|Ps]) -> + K1 = type(K), + V1 = type(V), + [{type,Line,map_field_assoc,[K1,V1]}|map_pair_types(Ps)]; +map_pair_types([{type,Line,map_field_exact,[K,V]}|Ps]) -> + K1 = type(K), + V1 = type(V), + [{type,Line,map_field_exact,[K1,V1]}|map_pair_types(Ps)]; +map_pair_types([]) -> []. + +field_types([{type,Line,field_type,[{atom,La,A},T]}|Fs]) -> + T1 = type(T), + [{type,Line,field_type,[{atom,La,A},T1]}|field_types(Fs)]; +field_types([]) -> []. + +type_list([T|Ts]) -> + T1 = type(T), + [T1|type_list(Ts)]; +type_list([]) -> []. diff --git a/test/category_SUITE.erl b/test/category_SUITE.erl index d927e61..5ea2c67 100644 --- a/test/category_SUITE.erl +++ b/test/category_SUITE.erl @@ -736,6 +736,8 @@ laws_kleisli_associativity_2(_) -> ] ). +zero() -> 0. + %% %% transformer_identity_unit(_) -> @@ -766,7 +768,7 @@ transformer_identity_try(_) -> 1 = ?tryT(identity, 1), a = ?tryT(identity, throw(a)), ok = try ?tryT(identity, exit(badarg)) catch _:badarg -> ok end, - ok = try ?tryT(identity, 1 / 0) catch _:badarith -> ok end. + ok = try ?tryT(identity, 1 / zero()) catch _:badarith -> ok end. %% %% @@ -800,7 +802,7 @@ transformer_option_try(_) -> undefined = ?tryT(option, undefined), a = ?tryT(option, throw(a)), undefined = ?tryT(option, exit(badarg)), - undefined = ?tryT(option, 1 / 0). + undefined = ?tryT(option, 1 / zero()). @@ -837,7 +839,7 @@ transformer_undefined_try(_) -> undefined = ?tryT(undefined, undefined), a = ?tryT(undefined, throw(a)), undefined = ?tryT(undefined, exit(badarg)), - undefined = ?tryT(undefined, 1 / 0). + undefined = ?tryT(undefined, 1 / zero()). %% @@ -888,7 +890,7 @@ transformer_either_try(_) -> {error, badarg} = ?tryT(either, {error, badarg}), {ok, a} = ?tryT(either, throw(a)), {error, badarg} = ?tryT(either, exit(badarg)), - {error, badarith} = ?tryT(either, 1 / 0). + {error, badarith} = ?tryT(either, 1 / zero()). %% @@ -939,7 +941,7 @@ transformer_reader_try(_) -> {error, badarg} = (?tryT(reader, {error, badarg}))(#{}), {ok, a} = (?tryT(reader, throw(a)))(#{}), {error, badarg} = (?tryT(reader, exit(badarg)))(#{}), - {error, badarith} = (?tryT(reader, 1 / 0))(#{}). + {error, badarith} = (?tryT(reader, 1 / zero()))(#{}). %% diff --git a/test/generic_SUITE.erl b/test/generic_SUITE.erl new file mode 100644 index 0000000..b97b463 --- /dev/null +++ b/test/generic_SUITE.erl @@ -0,0 +1,104 @@ +%% +%% Copyright (c) 2016, Dmitry Kolesnikov +%% All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% @doc +%% category pattern test suite +-module(generic_SUITE). +-include_lib("common_test/include/ct.hrl"). +-compile({parse_transform, generic}). + +-export([all/0]). +-export([ + struct_to_generic/1 +, generic_to_struct/1 +, struct_to_labeled/1 +, labeled_to_struct/1 + +]). + +-record(adt, {a, b, c}). + +all() -> + [Test || {Test, NAry} <- ?MODULE:module_info(exports), + Test =/= module_info, + Test =/= init_per_suite, + Test =/= end_per_suite, + NAry =:= 1 + ]. + +%% +%% +struct_to_generic(_) -> + #{ + a := 1, + b := <<"test">>, + c := 2.0 + } = generic:to(#adt{a = 1, b = <<"test">>, c = 2.0}), + + X = #adt{a = 1, b = <<"test">>, c = 2.0}, + #{ + a := 1, + b := <<"test">>, + c := 2.0 + } = generic:to(X#adt{}). + +%% +%% +generic_to_struct(_) -> + #adt{ + a = 1, + b = <<"test">>, + c = 2.0 + } = generic:adt(#{a => 1, b => <<"test">>, c => 2.0}), + + X = #{a => 1, b => <<"test">>, c => 2.0}, + #adt{ + a = 1, + b = <<"test">>, + c = 2.0 + } = generic:adt(X). + +%% +%% +struct_to_labeled(_) -> + #{ + <<"a">> := 1, + <<"b">> := <<"test">>, + <<"c">> := 2.0 + } = genericL:to(#adt{a = 1, b = <<"test">>, c = 2.0}), + + X = #adt{a = 1, b = <<"test">>, c = 2.0}, + #{ + <<"a">> := 1, + <<"b">> := <<"test">>, + <<"c">> := 2.0 + } = genericL:to(X#adt{}). + +%% +%% +labeled_to_struct(_) -> + #adt{ + a = 1, + b = <<"test">>, + c = 2.0 + } = genericL:adt(#{<<"a">> => 1, <<"b">> => <<"test">>, <<"c">> => 2.0}), + + X = #{<<"a">> => 1, <<"b">> => <<"test">>, <<"c">> => 2.0}, + #adt{ + a = 1, + b = <<"test">>, + c = 2.0 + } = genericL:adt(X). From 66040ac392d4efc8853fa0297de3a3874eff08bd Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Fri, 26 Apr 2019 23:13:51 +0300 Subject: [PATCH 03/18] Update documentation about generic type --- README.md | 13 ++++--- doc/features.md | 29 ++++++++++++++ rebar.config | 2 +- src/generic.erl | 86 ++++++------------------------------------ test/cover.spec | 2 - test/generic_SUITE.erl | 58 ++++++++++++---------------- test/tests.config | 8 ---- 7 files changed, 73 insertions(+), 125 deletions(-) delete mode 100644 test/cover.spec diff --git a/README.md b/README.md index c2df52f..d923afb 100644 --- a/README.md +++ b/README.md @@ -17,16 +17,17 @@ The [feature overview](doc/features.md) provides an introduction to datum features, use-cases and reasoning of they existence: -* data structures with common behavior: **foldable**, **traversable** and **map-like** including -* pure functional **data types**: binary search tree, red-black tree, heap, queues, streams and others +* notation for `option` and `either` types +* data structures with common behavior: **foldable**, **traversable** and **map-like**. +* pure functional **data types**: binary search tree, red-black tree, heap, queues, and others +* **streams** or lazy lists are a sequential data structure that contains on demand computed elements. * resembles concept of getters and setters ([**lens**](doc/lens.md)) for complex algebraic data types. -* define a **category pattern** and **monads** -* [type casts](doc/typecast.md) of scalar (primitive) data types +* define a **category pattern**, **monads** and they composition for Erlang applications +* [**typecasts**](doc/typecast.md) of primitive data types +* mapping of algebraic data types to they **generic** representation and back * supports **OTP/18.x** or later release -**Cheat Sheet** is available [here](doc/cheatsheet.md) - ## Getting started The latest version of the library is available at its `master` branch. All development, including new features and bug fixes, take place on the `master` branch using forking and pull requests as described in contribution guidelines. diff --git a/doc/features.md b/doc/features.md index 3b2ca49..1db9d40 100644 --- a/doc/features.md +++ b/doc/features.md @@ -8,6 +8,7 @@ * [Pure functional data types](#pure-functional-data-types) * [Stream](#stream) * [Lens](#lens) +* [Generic](#generic) * [Category pattern](#category-pattern) * [Monad](#monad) @@ -204,6 +205,34 @@ end. lens:get(Lens(a), lens:put(Lens(a), 1, bst:new())). ``` +## Generic + +Automatic transformation of algebraic data types to they generic representation solve wide problems of generic programming. Erlang has strong foundation and language primitives on this aspect due to ducked-typed nature. Existence of generic types (lists, maps and tuples) makes a programming task trivial. However, absence of strong types makes it error prone while switching a context from generic algorithms to business domain. Thus usage of algebraic data types (encoded in Erlang records) improves maintainability of domain specific code. + +> The main advantage of using records rather than tuples is that fields in a record are accessed by name, whereas fields in a tuple are accessed by position. + +The library introduces a `generic` parse transform that implements automatic mapping between ADTs and generic representations. + +```erlang +-compile({parse_transform, generic}). + +-record(person, {name, address, city}). + +example() -> + %% + %% builds a generic representation from #person{} ADT + Gen = generic:from(#person{ + name = "Verner Pleishner", + address = "Blumenstrasse 14", + city = "Berne" + }), + + %% + %% builds #person{} ADT from generic representation + generic:person(Gen). +``` + +In facts, a macro magic happens behind to execute transformation without boilerplate. As the result a labeled generic representation is returned. ## Category pattern diff --git a/rebar.config b/rebar.config index d44ba1c..cffb204 100644 --- a/rebar.config +++ b/rebar.config @@ -31,5 +31,5 @@ {plugins , [coveralls]}. {cover_enabled , true}. {cover_export_enabled , true}. -{coveralls_coverdata , "/tmp/test/datum/ct.coverdata"}. +{coveralls_coverdata , "_build/test/cover/ct.coverdata"}. {coveralls_service_name , "travis-ci"}. diff --git a/src/generic.erl b/src/generic.erl index 1099c86..0d49e3b 100644 --- a/src/generic.erl +++ b/src/generic.erl @@ -4,17 +4,15 @@ -module(generic). -export([ - to/2 -, toL/2 -, from/3 -, fromL/3 + from/2 +, to/3 , parse_transform/2 ]). --spec to([atom()], tuple()) -> map(). +-spec from([atom()], tuple()) -> map(). -to(Fields, Struct) -> +from(Fields, Struct) -> maps:from_list( [{Key, Value} || {Key, Value} <- lists:zip( @@ -26,32 +24,11 @@ to(Fields, Struct) -> ] ). --spec toL([atom()], tuple()) -> map(). +-spec to(atom(), [atom()], map()) -> tuple(). -toL(Fields, Struct) -> - maps:from_list( - [{typecast:s(Key), Value} || - {Key, Value} <- lists:zip( - Fields, - tl(tuple_to_list(Struct)) - ), - Value /= undefined, - Value /= null - ] - ). - --spec from(atom(), [atom()], map()) -> tuple(). - -from(Type, Fields, Struct) -> +to(Type, Fields, Struct) -> list_to_tuple([Type | - [maps:get(X, Struct, undefined) || X <- Fields]] - ). - --spec fromL(atom(), [atom()], map()) -> tuple(). - -fromL(Type, Fields, Struct) -> - list_to_tuple([Type | - [maps:get(typecast:s(X), Struct, undefined) || X <- Fields]] + [maps:get(X, Struct, undefined) || X <- Fields]] ). %%%------------------------------------------------------------------ @@ -543,37 +520,11 @@ expr({named_fun,Loc,Name,Cs}) -> %% %% expr({call, Ln, - {remote, _, {atom, _, generic}, {atom, _, to}}, - [{record, _, {var, _, _} = Struct, Type, _}] -}) -> - {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, to}}, - [ - {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, - expr(Struct) - ] - }; - -expr({call, Ln, - {remote, _, {atom, _, generic}, {atom, _, to}}, - [{record, _, Type, _} = Struct] -}) -> - {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, to}}, - [ - {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, - expr(Struct) - ] - }; - -%% -%% -expr({call, Ln, - {remote, _, {atom, _, genericL}, {atom, _, to}}, + {remote, _, {atom, _, generic}, {atom, _, from}}, [{record, _, {var, _, _} = Struct, Type, _}] }) -> {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, toL}}, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, from}}, [ {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, expr(Struct) @@ -581,11 +532,11 @@ expr({call, Ln, }; expr({call, Ln, - {remote, _, {atom, _, genericL}, {atom, _, to}}, + {remote, _, {atom, _, generic}, {atom, _, from}}, [{record, _, Type, _} = Struct] }) -> {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, toL}}, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, from}}, [ {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, expr(Struct) @@ -599,20 +550,7 @@ expr({call, Ln, [Struct] }) -> {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, from}}, - [ - {atom, Ln, Type}, - {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, - expr(Struct) - ] - }; - -expr({call, Ln, - {remote, _, {atom, _, genericL}, {atom, _, Type}}, - [Struct] -}) -> - {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, fromL}}, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, to}}, [ {atom, Ln, Type}, {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, diff --git a/test/cover.spec b/test/cover.spec deleted file mode 100644 index 4e558ed..0000000 --- a/test/cover.spec +++ /dev/null @@ -1,2 +0,0 @@ -{incl_app, datum, details}. - diff --git a/test/generic_SUITE.erl b/test/generic_SUITE.erl index b97b463..288be34 100644 --- a/test/generic_SUITE.erl +++ b/test/generic_SUITE.erl @@ -22,11 +22,9 @@ -export([all/0]). -export([ - struct_to_generic/1 + syntax_generic/1 +, struct_to_generic/1 , generic_to_struct/1 -, struct_to_labeled/1 -, labeled_to_struct/1 - ]). -record(adt, {a, b, c}). @@ -39,6 +37,15 @@ all() -> NAry =:= 1 ]. +%% +%% +syntax_generic(_) -> + ok = transform("generic:from(#adt{a = 1})."), + ok = transform("generic:from(X#adt{})."), + ok = transform("generic:adt(#{a => 1})."), + ok = transform("generic:adt(X)."). + + %% %% struct_to_generic(_) -> @@ -46,14 +53,14 @@ struct_to_generic(_) -> a := 1, b := <<"test">>, c := 2.0 - } = generic:to(#adt{a = 1, b = <<"test">>, c = 2.0}), + } = generic:from(#adt{a = 1, b = <<"test">>, c = 2.0}), X = #adt{a = 1, b = <<"test">>, c = 2.0}, #{ a := 1, b := <<"test">>, c := 2.0 - } = generic:to(X#adt{}). + } = generic:from(X#adt{}). %% %% @@ -71,34 +78,17 @@ generic_to_struct(_) -> c = 2.0 } = generic:adt(X). -%% -%% -struct_to_labeled(_) -> - #{ - <<"a">> := 1, - <<"b">> := <<"test">>, - <<"c">> := 2.0 - } = genericL:to(#adt{a = 1, b = <<"test">>, c = 2.0}), - X = #adt{a = 1, b = <<"test">>, c = 2.0}, - #{ - <<"a">> := 1, - <<"b">> := <<"test">>, - <<"c">> := 2.0 - } = genericL:to(X#adt{}). +%%%------------------------------------------------------------------ +%%% +%%% helpers +%%% +%%%------------------------------------------------------------------ -%% -%% -labeled_to_struct(_) -> - #adt{ - a = 1, - b = <<"test">>, - c = 2.0 - } = genericL:adt(#{<<"a">> => 1, <<"b">> => <<"test">>, <<"c">> => 2.0}), +transform(Code) -> + {ok, Parsed, _} = erl_scan:string(Code), + {ok, Forms} = erl_parse:parse_exprs(Parsed), + Fun = [{function, 1, a, 1, [{clause, 1, [], [], Forms}]}], + [{function, _, _, _, _}] = generic:parse_transform(Fun, []), + ok. - X = #{<<"a">> => 1, <<"b">> => <<"test">>, <<"c">> => 2.0}, - #adt{ - a = 1, - b = <<"test">>, - c = 2.0 - } = genericL:adt(X). diff --git a/test/tests.config b/test/tests.config index 6c9fa79..0c306ce 100644 --- a/test/tests.config +++ b/test/tests.config @@ -1,7 +1,3 @@ -%% -%% logs -{logdir, "/tmp/test/datum/"}. - %% %% suites {suites, ".", all}. @@ -16,7 +12,3 @@ % stream_SUITE, % tree_SUITE % ], "debug"}. - -%% -%% code coverage -{cover, "cover.spec"}. From d7e52b6f609fa60093a540ce01d1aa8f4e0a1b29 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Fri, 26 Apr 2019 23:18:51 +0300 Subject: [PATCH 04/18] Support list generic representation --- src/generic.erl | 13 +++++++++---- test/generic_SUITE.erl | 8 +++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/generic.erl b/src/generic.erl index 0d49e3b..7bf510a 100644 --- a/src/generic.erl +++ b/src/generic.erl @@ -12,7 +12,8 @@ -spec from([atom()], tuple()) -> map(). -from(Fields, Struct) -> +from(Fields, Struct) + when is_tuple(Struct) -> maps:from_list( [{Key, Value} || {Key, Value} <- lists:zip( @@ -26,10 +27,14 @@ from(Fields, Struct) -> -spec to(atom(), [atom()], map()) -> tuple(). -to(Type, Fields, Struct) -> +to(Type, Fields, Generic) + when is_map(Generic) -> list_to_tuple([Type | - [maps:get(X, Struct, undefined) || X <- Fields]] - ). + [maps:get(X, Generic, undefined) || X <- Fields]] + ); +to(Type, Fields, Generic) + when is_list(Generic), length(Fields) =:= length(Generic) -> + list_to_tuple([Type | Generic]). %%%------------------------------------------------------------------ %%% diff --git a/test/generic_SUITE.erl b/test/generic_SUITE.erl index 288be34..cb9b204 100644 --- a/test/generic_SUITE.erl +++ b/test/generic_SUITE.erl @@ -76,7 +76,13 @@ generic_to_struct(_) -> a = 1, b = <<"test">>, c = 2.0 - } = generic:adt(X). + } = generic:adt(X), + + #adt{ + a = 1, + b = <<"test">>, + c = 2.0 + } = generic:adt([1, <<"test">>, 2.0]). %%%------------------------------------------------------------------ From 0ccc5ef1dc4fa8e4754159ae0d6b1813c11d03d0 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Sat, 27 Apr 2019 12:26:51 +0300 Subject: [PATCH 05/18] increase test coverage of generic module --- rebar.config | 30 +++++++++++++++--------------- test/generic_SUITE.erl | 8 ++++++-- test/tests.config | 11 ----------- 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/rebar.config b/rebar.config index cffb204..b4f5ac8 100644 --- a/rebar.config +++ b/rebar.config @@ -5,25 +5,25 @@ {erl_first_files, [ "src/category/category.erl" - ,"src/category/datum_cat.erl" - ,"src/category/datum_cat_f.erl" - ,"src/category/datum_cat_option.erl" - ,"src/category/datum_cat_undefined.erl" - ,"src/category/datum_cat_either.erl" - ,"src/category/datum_cat_reader.erl" - ,"src/category/datum_cat_kleisli.erl" +, "src/category/datum_cat.erl" +, "src/category/datum_cat_f.erl" +, "src/category/datum_cat_option.erl" +, "src/category/datum_cat_undefined.erl" +, "src/category/datum_cat_either.erl" +, "src/category/datum_cat_reader.erl" +, "src/category/datum_cat_kleisli.erl" - ,"src/monad/monad.erl" - ,"src/monad/datum_monad.erl" +, "src/monad/monad.erl" +, "src/monad/datum_monad.erl" - ,"src/partial.erl" +, "src/partial.erl" - ,"src/foldable.erl" - ,"src/maplike.erl" - ,"src/topological.erl" - ,"src/traversable.erl" +, "src/foldable.erl" +, "src/maplike.erl" +, "src/topological.erl" +, "src/traversable.erl" - ,"src/generic.erl" +, "src/generic.erl" ]}. %% diff --git a/test/generic_SUITE.erl b/test/generic_SUITE.erl index cb9b204..ab5680f 100644 --- a/test/generic_SUITE.erl +++ b/test/generic_SUITE.erl @@ -43,7 +43,12 @@ syntax_generic(_) -> ok = transform("generic:from(#adt{a = 1})."), ok = transform("generic:from(X#adt{})."), ok = transform("generic:adt(#{a => 1})."), - ok = transform("generic:adt(X)."). + ok = transform("generic:adt(X)."), + + ok = transform("-compile({parse_transform, generic})."), + ok = transform("-export([all/0])."), + ok = transform("a:b(), a:b(1), a:b(X), a:b(#t{}), a:b(#{})."), + ok = transform("b(), b(1), b(X), b(#t{}), b(#{})."). %% @@ -97,4 +102,3 @@ transform(Code) -> Fun = [{function, 1, a, 1, [{clause, 1, [], [], Forms}]}], [{function, _, _, _, _}] = generic:parse_transform(Fun, []), ok. - diff --git a/test/tests.config b/test/tests.config index 0c306ce..1e7f377 100644 --- a/test/tests.config +++ b/test/tests.config @@ -1,14 +1,3 @@ %% %% suites {suites, ".", all}. -% {skip_suites, ".", [ -% foldable_SUITE, -% maplike_SUITE, -% traversable_SUITE, - -% category_SUITE, -% lens_SUITE, -% m_SUITE, -% stream_SUITE, -% tree_SUITE -% ], "debug"}. From 2e224953ff2941186108c0a8a98b0ad33c7ba457 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Sat, 27 Apr 2019 18:06:42 +0300 Subject: [PATCH 06/18] Update generic build config --- beam.mk | 3 +- rebar.config | 8 ++++ src/generic.erl | 103 +++++++++++++++++++++-------------------- test/generic_SUITE.erl | 7 +-- 4 files changed, 65 insertions(+), 56 deletions(-) diff --git a/beam.mk b/beam.mk index b18cb28..4cffd01 100644 --- a/beam.mk +++ b/beam.mk @@ -90,7 +90,8 @@ compile: rebar3 ## ## execute common test and terminate node test: - @./rebar3 ct --config=test/${TEST}.config --cover --verbose +# @./rebar3 ct --config test/${TEST}.config --cover --verbose + @./rebar3 ct --cover --verbose @./rebar3 cover # test: _build/test.beam diff --git a/rebar.config b/rebar.config index b4f5ac8..5d56145 100644 --- a/rebar.config +++ b/rebar.config @@ -26,6 +26,14 @@ , "src/generic.erl" ]}. +%% +%% Exclude parse transform from cover reports +{cover_excl_mods, [ + category +, partial +, generic +]}. + %% %% {plugins , [coveralls]}. diff --git a/src/generic.erl b/src/generic.erl index 7bf510a..d49e231 100644 --- a/src/generic.erl +++ b/src/generic.erl @@ -36,6 +36,58 @@ to(Type, Fields, Generic) when is_list(Generic), length(Fields) =:= length(Generic) -> list_to_tuple([Type | Generic]). +%% +%% +-spec hook_generic(_) -> _. + +%% +%% +hook_generic({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, from}}, + [{record, _, {var, _, _} = Struct, Type, _}] +}) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, from}}, + [ + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + expr(Struct) + ] + }; + +hook_generic({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, from}}, + [{record, _, Type, _} = Struct] +}) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, from}}, + [ + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + expr(Struct) + ] + }; + +hook_generic({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, Type}}, + [Struct] +}) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, to}}, + [ + {atom, Ln, Type}, + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + expr(Struct) + ] + }; + +hook_generic({call,Line,F0,As0}) -> + %% N.B. If F an atom then call to local function or BIF, if F a + %% remote structure (see below) then call to other module, + %% otherwise apply to "function". + F1 = expr(F0), + As1 = expr_list(As0), + {call,Line,F1,As1}. + + %%%------------------------------------------------------------------ %%% %%% stdlib-3.0/examples/erl_id_trans.erl @@ -521,55 +573,8 @@ expr({'fun',Line,Body}) -> end; expr({named_fun,Loc,Name,Cs}) -> {named_fun,Loc,Name,fun_clauses(Cs)}; - -%% -%% -expr({call, Ln, - {remote, _, {atom, _, generic}, {atom, _, from}}, - [{record, _, {var, _, _} = Struct, Type, _}] -}) -> - {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, from}}, - [ - {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, - expr(Struct) - ] - }; - -expr({call, Ln, - {remote, _, {atom, _, generic}, {atom, _, from}}, - [{record, _, Type, _} = Struct] -}) -> - {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, from}}, - [ - {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, - expr(Struct) - ] - }; - -%% -%% -expr({call, Ln, - {remote, _, {atom, _, generic}, {atom, _, Type}}, - [Struct] -}) -> - {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, to}}, - [ - {atom, Ln, Type}, - {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, - expr(Struct) - ] - }; - -expr({call,Line,F0,As0}) -> - %% N.B. If F an atom then call to local function or BIF, if F a - %% remote structure (see below) then call to other module, - %% otherwise apply to "function". - F1 = expr(F0), - As1 = expr_list(As0), - {call,Line,F1,As1}; +expr({call, _, _, _} = Call) -> + hook_generic(Call); expr({'catch',Line,E0}) -> %% No new variables added. E1 = expr(E0), diff --git a/test/generic_SUITE.erl b/test/generic_SUITE.erl index ab5680f..d5bac16 100644 --- a/test/generic_SUITE.erl +++ b/test/generic_SUITE.erl @@ -44,12 +44,7 @@ syntax_generic(_) -> ok = transform("generic:from(X#adt{})."), ok = transform("generic:adt(#{a => 1})."), ok = transform("generic:adt(X)."), - - ok = transform("-compile({parse_transform, generic})."), - ok = transform("-export([all/0])."), - ok = transform("a:b(), a:b(1), a:b(X), a:b(#t{}), a:b(#{})."), - ok = transform("b(), b(1), b(X), b(#t{}), b(#{})."). - + ok = transform("a:b(X)."). %% %% From 654dde95e0f9a41019dd6eb0faa93feac67d3fac Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Sat, 27 Apr 2019 23:22:40 +0300 Subject: [PATCH 07/18] Support product lense (lens:p) for ADTs --- src/lens/lens.erl | 36 ++++++++++++--- test/lens_SUITE.erl | 106 +++++++++----------------------------------- 2 files changed, 52 insertions(+), 90 deletions(-) diff --git a/src/lens/lens.erl b/src/lens/lens.erl index 3cd5ddb..7453eb5 100644 --- a/src/lens/lens.erl +++ b/src/lens/lens.erl @@ -517,17 +517,41 @@ c(Ln9, Ln8, Ln7, Ln6, Ln5, Ln4, Ln3, Ln2, Ln1) -> p(Lenses) when is_list(Lenses) -> fun(Fun, Struct) -> - fmap(put_lens_product(Lenses, _, Struct), Fun(get_lens_product(Lenses, Struct))) + fmap(p_put(Lenses, _, Struct), Fun(p_get(Lenses, Struct))) + end; + +p(Lenses) + when is_tuple(Lenses) -> + fun(Fun, Struct) -> + fmap( + p_put_struct(Lenses, _, Struct), + Fun(list_to_tuple(p_get(tuple_to_list(Lenses), Struct))) + ) end. -get_lens_product(Lenses, Struct) -> - [lens:get(LnX, Struct) || LnX <- Lenses]. +%% +p_get([Lens | Lenses], Struct) + when is_function(Lens) -> + [lens:get(Lens, Struct) | p_get(Lenses, Struct)]; -put_lens_product([Lens | Lenses], [X | View], Struct) -> - put_lens_product(Lenses, View, lens:put(Lens, X, Struct)); -put_lens_product([], [], Struct) -> +p_get([Value | Lenses], Struct) -> + [Value | p_get(Lenses, Struct)]; + +p_get([], _) -> + []. + +%% +p_put([Lens | Lenses], [X | View], Struct) + when is_function(Lens) -> + p_put(Lenses, View, lens:put(Lens, X, Struct)); +p_put([_Lens | Lenses], [_X | View], Struct) -> + p_put(Lenses, View, Struct); +p_put([], [], Struct) -> Struct. +%% +p_put_struct(Lenses, View, Struct) -> + p_put(erlang:tuple_to_list(Lenses), erlang:tuple_to_list(View), Struct). %% %% Inline variants of lens product combinator. diff --git a/test/lens_SUITE.erl b/test/lens_SUITE.erl index 3486b50..0e69e88 100644 --- a/test/lens_SUITE.erl +++ b/test/lens_SUITE.erl @@ -29,19 +29,7 @@ -module(lens_SUITE). -include_lib("common_test/include/ct.hrl"). -%% -%% common test --export([ - all/0 - ,groups/0 - ,init_per_suite/1 - ,end_per_suite/1 - ,init_per_group/2 - ,end_per_group/2 -]). - -%% -%% pure lens interface +-export([all/0]). -export([ id/1, const/1, @@ -83,12 +71,13 @@ compose8/1, compose9/1, - product1/1, - product2/1, - product3/1, - product4/1, - product5/1, - product6/1, + product1_with_list/1, + product1_with_tuple/1, + product2/1, + product3/1, + product4/1, + product5/1, + product6/1, product7/1, product8/1, product9/1, @@ -108,72 +97,13 @@ %%% %%%---------------------------------------------------------------------------- all() -> - [ - {group, basic}, - {group, binary}, - {group, list}, - {group, tuple}, - {group, map}, - {group, pair}, - {group, keylist}, - {group, unittest}, - {group, compose}, - {group, lens_api} - ]. - -groups() -> - [ - {basic, [parallel], - [id, const]}, - - {binary, [parallel], - [hbits, tbits, bits]}, - - {list, [parallel], - [hd, hd_om, tl, tl_om, traverse, traverse_empty, takewith, takewith_om]}, - - {tuple, [parallel], - [t1, t2, t3, ti]}, - - {map, [parallel], - [at, at_om]}, - - {pair, [parallel], - [pair, pair_om]}, - - {keylist, [parallel], - [keylist, keylist_om]}, - - {unittest, [parallel], - [require, defined]}, - - {compose, [parallel], - [compose1, compose2, compose3, compose4, compose5, compose6, compose7, compose8, compose9, - product1, product2, product3, product4, product5, product6, product7, product8, product9]}, - - {lens_api, [parallel], - [get, put, map, apply, iso_pairs, iso_lists, iso4]} + [Test || {Test, NAry} <- ?MODULE:module_info(exports), + Test =/= module_info, + Test =/= init_per_suite, + Test =/= end_per_suite, + NAry =:= 1 ]. -%%%---------------------------------------------------------------------------- -%%% -%%% init -%%% -%%%---------------------------------------------------------------------------- -init_per_suite(Config) -> - Config. - -end_per_suite(_Config) -> - ok. - -%% -%% -init_per_group(_, Config) -> - Config. - -end_per_group(_, _Config) -> - ok. - %%%---------------------------------------------------------------------------- %%% %%% pure lens interface @@ -548,7 +478,7 @@ compose9(_Config) -> law_put_put(Lens, a, b, [a, {[{[{[{b}]}], 2}],2}], Data). -product1(_Config) -> +product1_with_list(_Config) -> Lens = lens:p([lens:at(a), lens:at(b)]), Data = #{a => 1, b => 2}, [1, 2] = lens:get(Lens, Data), @@ -556,6 +486,14 @@ product1(_Config) -> law_put_get(Lens, [a, b], Data), law_put_put(Lens, [a, b], [-1, -2], #{a => -1, b => -2}, Data). +product1_with_tuple(_Config) -> + Lens = lens:p({a, lens:at(a), lens:at(b)}), + Data = #{a => 1, b => 2}, + {a, 1, 2} = lens:get(Lens, Data), + law_get_put(Lens, Data), + law_put_get(Lens, {a, a, b}, Data), + law_put_put(Lens, {a, a, b}, {a, -1, -2}, #{a => -1, b => -2}, Data). + product2(_Config) -> Lens = lens:p(lens:at(a), lens:at(b)), Data = #{a => 1, b => 2}, From fa6f3c38c3da8ce12ce7f3bed23868b9ca4a9c48 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Sat, 27 Apr 2019 23:35:26 +0300 Subject: [PATCH 08/18] Support sequence of generic --- src/generic.erl | 29 +++++++++++++++++++++---- test/generic_SUITE.erl | 49 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/generic.erl b/src/generic.erl index d49e231..5e6b4de 100644 --- a/src/generic.erl +++ b/src/generic.erl @@ -10,6 +10,8 @@ ]). +%% +%% -spec from([atom()], tuple()) -> map(). from(Fields, Struct) @@ -23,8 +25,14 @@ from(Fields, Struct) Value /= undefined, Value /= null ] - ). + ); +from(Fields, Structs) + when is_list(Structs) -> + [from(Fields, Struct) || Struct <- Structs]. + +%% +%% -spec to(atom(), [atom()], map()) -> tuple(). to(Type, Fields, Generic) @@ -32,9 +40,10 @@ to(Type, Fields, Generic) list_to_tuple([Type | [maps:get(X, Generic, undefined) || X <- Fields]] ); -to(Type, Fields, Generic) - when is_list(Generic), length(Fields) =:= length(Generic) -> - list_to_tuple([Type | Generic]). + +to(Type, Fields, Generics) + when is_list(Generics) -> + [to(Type, Fields, Generic) || Generic <- Generics]. %% %% @@ -66,6 +75,18 @@ hook_generic({call, Ln, ] }; +hook_generic({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, from}}, + [{cons, Ln, {record, _, Type, _}, _} = Struct] +}) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, from}}, + [ + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + expr(Struct) + ] + }; + hook_generic({call, Ln, {remote, _, {atom, _, generic}, {atom, _, Type}}, [Struct] diff --git a/test/generic_SUITE.erl b/test/generic_SUITE.erl index d5bac16..073a37b 100644 --- a/test/generic_SUITE.erl +++ b/test/generic_SUITE.erl @@ -25,6 +25,8 @@ syntax_generic/1 , struct_to_generic/1 , generic_to_struct/1 +, structs_to_generic/1 +, generic_to_structs/1 ]). -record(adt, {a, b, c}). @@ -76,13 +78,52 @@ generic_to_struct(_) -> a = 1, b = <<"test">>, c = 2.0 - } = generic:adt(X), + } = generic:adt(X). - #adt{ + % #adt{ + % a = 1, + % b = <<"test">>, + % c = 2.0 + % } = generic:adt([1, <<"test">>, 2.0]). + + +%% +%% +structs_to_generic(_) -> + [#{ + a := 1, + b := <<"test">>, + c := 2.0 + }] = generic:from([#adt{a = 1, b = <<"test">>, c = 2.0}]), + + X = [#adt{a = 1, b = <<"test">>, c = 2.0}], + [#{ + a := 1, + b := <<"test">>, + c := 2.0 + }] = generic:from(X#adt{}). + +%% +%% +generic_to_structs(_) -> + [#adt{ + a = 1, + b = <<"test">>, + c = 2.0 + }] = generic:adt([#{a => 1, b => <<"test">>, c => 2.0}]), + + X = [#{a => 1, b => <<"test">>, c => 2.0}], + [#adt{ a = 1, b = <<"test">>, - c = 2.0 - } = generic:adt([1, <<"test">>, 2.0]). + c = 2.0 + }] = generic:adt(X). + + % #adt{ + % a = 1, + % b = <<"test">>, + % c = 2.0 + % } = generic:adt([1, <<"test">>, 2.0]). %%%------------------------------------------------------------------ From dab22a1cdffbfc41e478327d1741841d9bbfdf16 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Sat, 4 May 2019 23:28:11 +0300 Subject: [PATCH 09/18] Add derived codecs --- src/generic.erl | 45 ++++++++++++++++++++ test/generic_SUITE.erl | 95 +++++++++--------------------------------- 2 files changed, 64 insertions(+), 76 deletions(-) diff --git a/src/generic.erl b/src/generic.erl index 5e6b4de..3d87f70 100644 --- a/src/generic.erl +++ b/src/generic.erl @@ -51,6 +51,51 @@ to(Type, Fields, Generics) %% %% +hook_generic({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, encode}}, + [{record, _, Type, _}] +}) -> + {'fun', Ln, + {clauses, [ + {clause, Ln, + [{var, Ln, 'X'}], + [], + [ + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, from}}, + [ + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + {var, Ln, 'X'} + ] + } + ] + } + ]} + }; + +hook_generic({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, decode}}, + [{record, _, Type, _}] +}) -> + {'fun', Ln, + {clauses, [ + {clause, Ln, + [{var, Ln, 'X'}], + [], + [ + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, to}}, + [ + {atom, Ln, Type}, + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + {var, Ln, 'X'} + ] + } + ] + } + ]} + }; + hook_generic({call, Ln, {remote, _, {atom, _, generic}, {atom, _, from}}, [{record, _, {var, _, _} = Struct, Type, _}] diff --git a/test/generic_SUITE.erl b/test/generic_SUITE.erl index 073a37b..d017370 100644 --- a/test/generic_SUITE.erl +++ b/test/generic_SUITE.erl @@ -22,11 +22,9 @@ -export([all/0]). -export([ - syntax_generic/1 -, struct_to_generic/1 -, generic_to_struct/1 -, structs_to_generic/1 -, generic_to_structs/1 + syntax/1 +, generic/1 +, derived/1 ]). -record(adt, {a, b, c}). @@ -41,90 +39,35 @@ all() -> %% %% -syntax_generic(_) -> +syntax(_) -> ok = transform("generic:from(#adt{a = 1})."), ok = transform("generic:from(X#adt{})."), ok = transform("generic:adt(#{a => 1})."), ok = transform("generic:adt(X)."), + ok = transform("generic:encode(#adt{})."), + ok = transform("generic:decode(#adt{})."), ok = transform("a:b(X)."). %% %% -struct_to_generic(_) -> - #{ - a := 1, - b := <<"test">>, - c := 2.0 - } = generic:from(#adt{a = 1, b = <<"test">>, c = 2.0}), +generic(_) -> + Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, + Structs = [Struct], - X = #adt{a = 1, b = <<"test">>, c = 2.0}, - #{ - a := 1, - b := <<"test">>, - c := 2.0 - } = generic:from(X#adt{}). + #{} = generic:from(Struct#adt{}), + [#{}] = generic:from(Structs#adt{}), + Struct = generic:adt(generic:from(Struct#adt{})), + Structs= generic:adt(generic:from(Structs#adt{})). %% %% -generic_to_struct(_) -> - #adt{ - a = 1, - b = <<"test">>, - c = 2.0 - } = generic:adt(#{a => 1, b => <<"test">>, c => 2.0}), - - X = #{a => 1, b => <<"test">>, c => 2.0}, - #adt{ - a = 1, - b = <<"test">>, - c = 2.0 - } = generic:adt(X). - - % #adt{ - % a = 1, - % b = <<"test">>, - % c = 2.0 - % } = generic:adt([1, <<"test">>, 2.0]). - - -%% -%% -structs_to_generic(_) -> - [#{ - a := 1, - b := <<"test">>, - c := 2.0 - }] = generic:from([#adt{a = 1, b = <<"test">>, c = 2.0}]), - - X = [#adt{a = 1, b = <<"test">>, c = 2.0}], - [#{ - a := 1, - b := <<"test">>, - c := 2.0 - }] = generic:from(X#adt{}). - -%% -%% -generic_to_structs(_) -> - [#adt{ - a = 1, - b = <<"test">>, - c = 2.0 - }] = generic:adt([#{a => 1, b => <<"test">>, c => 2.0}]), - - X = [#{a => 1, b => <<"test">>, c => 2.0}], - [#adt{ - a = 1, - b = <<"test">>, - c = 2.0 - }] = generic:adt(X). - - % #adt{ - % a = 1, - % b = <<"test">>, - % c = 2.0 - % } = generic:adt([1, <<"test">>, 2.0]). +derived(_) -> + Encode = generic:encode(#adt{}), + Decode = generic:decode(#adt{}), + Expect = #adt{a = 1, b = <<"test">>, c = 2.0}, + Expect = Decode(Encode(Expect)), + [Expect] = Decode(Encode([Expect])). %%%------------------------------------------------------------------ %%% From f4a00d725c40893845e9fea11d1189e9e8ecfc5e Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Sun, 5 May 2019 16:11:08 +0300 Subject: [PATCH 10/18] support derived codec for labelled generic --- src/generic.erl | 125 +++++++++++++++++++++++++++++------------ test/generic_SUITE.erl | 35 ++++++++++-- 2 files changed, 119 insertions(+), 41 deletions(-) diff --git a/src/generic.erl b/src/generic.erl index 3d87f70..d500ac6 100644 --- a/src/generic.erl +++ b/src/generic.erl @@ -6,6 +6,8 @@ -export([ from/2 , to/3 +, labelled_from/2 +, labelled_to/3 , parse_transform/2 ]). @@ -45,6 +47,41 @@ to(Type, Fields, Generics) when is_list(Generics) -> [to(Type, Fields, Generic) || Generic <- Generics]. +%% +%% +-spec labelled_from([atom()], tuple()) -> map(). + +labelled_from(Fields, Struct) + when is_tuple(Struct) -> + maps:from_list( + [{typecast:s(Key), Value} || + {Key, Value} <- lists:zip( + Fields, + tl(tuple_to_list(Struct)) + ), + Value /= undefined, + Value /= null + ] + ); + +labelled_from(Fields, Structs) + when is_list(Structs) -> + [labelled_from(Fields, Struct) || Struct <- Structs]. + +%% +%% +-spec labelled_to(atom(), [atom()], map()) -> tuple(). + +labelled_to(Type, Fields, Generic) + when is_map(Generic) -> + list_to_tuple([Type | + [maps:get(typecast:s(X), Generic, undefined) || X <- Fields]] + ); + +labelled_to(Type, Fields, Generics) + when is_list(Generics) -> + [labelled_to(Type, Fields, Generic) || Generic <- Generics]. + %% %% -spec hook_generic(_) -> _. @@ -55,46 +92,25 @@ hook_generic({call, Ln, {remote, _, {atom, _, generic}, {atom, _, encode}}, [{record, _, Type, _}] }) -> - {'fun', Ln, - {clauses, [ - {clause, Ln, - [{var, Ln, 'X'}], - [], - [ - {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, from}}, - [ - {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, - {var, Ln, 'X'} - ] - } - ] - } - ]} - }; + cc_encode(Ln, from, Type); + +hook_generic({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, lencode}}, + [{record, _, Type, _}] +}) -> + cc_encode(Ln, labelled_from, Type); hook_generic({call, Ln, {remote, _, {atom, _, generic}, {atom, _, decode}}, [{record, _, Type, _}] }) -> - {'fun', Ln, - {clauses, [ - {clause, Ln, - [{var, Ln, 'X'}], - [], - [ - {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, to}}, - [ - {atom, Ln, Type}, - {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, - {var, Ln, 'X'} - ] - } - ] - } - ]} - }; + cc_decode(Ln, to, Type); + +hook_generic({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, ldecode}}, + [{record, _, Type, _}] +}) -> + cc_decode(Ln, labelled_to, Type); hook_generic({call, Ln, {remote, _, {atom, _, generic}, {atom, _, from}}, @@ -154,6 +170,45 @@ hook_generic({call,Line,F0,As0}) -> {call,Line,F1,As1}. +cc_encode(Ln, With, Type) -> + {'fun', Ln, + {clauses, [ + {clause, Ln, + [{var, Ln, 'X'}], + [], + [ + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, With}}, + [ + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + {var, Ln, 'X'} + ] + } + ] + } + ]} + }. + +cc_decode(Ln, With, Type) -> + {'fun', Ln, + {clauses, [ + {clause, Ln, + [{var, Ln, 'X'}], + [], + [ + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, With}}, + [ + {atom, Ln, Type}, + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + {var, Ln, 'X'} + ] + } + ] + } + ]} + }. + %%%------------------------------------------------------------------ %%% %%% stdlib-3.0/examples/erl_id_trans.erl diff --git a/test/generic_SUITE.erl b/test/generic_SUITE.erl index d017370..d9b4626 100644 --- a/test/generic_SUITE.erl +++ b/test/generic_SUITE.erl @@ -25,6 +25,7 @@ syntax/1 , generic/1 , derived/1 +, derived_labelled/1 ]). -record(adt, {a, b, c}). @@ -52,22 +53,44 @@ syntax(_) -> %% generic(_) -> Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, + Expect = #{a => 1, b => <<"test">>, c => 2.0}, Structs = [Struct], - #{} = generic:from(Struct#adt{}), - [#{}] = generic:from(Structs#adt{}), + Expect = generic:from(Struct#adt{}), + [Expect] = generic:from(Structs#adt{}), Struct = generic:adt(generic:from(Struct#adt{})), - Structs= generic:adt(generic:from(Structs#adt{})). + Structs = generic:adt(generic:from(Structs#adt{})). %% %% derived(_) -> + Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, + Expect = #{a => 1, b => <<"test">>, c => 2.0}, + Encode = generic:encode(#adt{}), Decode = generic:decode(#adt{}), - Expect = #adt{a = 1, b = <<"test">>, c = 2.0}, - Expect = Decode(Encode(Expect)), - [Expect] = Decode(Encode([Expect])). + Expect = Encode(Struct), + [Expect] = Encode([Struct]), + + Struct = Decode(Encode(Struct)), + [Struct] = Decode(Encode([Struct])). + +%% +%% +derived_labelled(_) -> + Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, + Expect = #{<<"a">> => 1, <<"b">> => <<"test">>, <<"c">> => 2.0}, + + Encode = generic:lencode(#adt{}), + Decode = generic:ldecode(#adt{}), + + Expect = Encode(Struct), + [Expect] = Encode([Struct]), + + Struct = Decode(Encode(Struct)), + [Struct] = Decode(Encode([Struct])). + %%%------------------------------------------------------------------ %%% From ff68851743e0c32831804772994874d9d0d1fb01 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Sun, 5 May 2019 20:44:04 +0300 Subject: [PATCH 11/18] Split labelled feature from generic interface --- src/generic.erl | 93 +++++++++++++++++++++++++++++------------- test/generic_SUITE.erl | 28 ++++++++++--- 2 files changed, 87 insertions(+), 34 deletions(-) diff --git a/src/generic.erl b/src/generic.erl index d500ac6..44f8472 100644 --- a/src/generic.erl +++ b/src/generic.erl @@ -94,66 +94,92 @@ hook_generic({call, Ln, }) -> cc_encode(Ln, from, Type); -hook_generic({call, Ln, - {remote, _, {atom, _, generic}, {atom, _, lencode}}, - [{record, _, Type, _}] -}) -> - cc_encode(Ln, labelled_from, Type); - hook_generic({call, Ln, {remote, _, {atom, _, generic}, {atom, _, decode}}, [{record, _, Type, _}] }) -> cc_decode(Ln, to, Type); -hook_generic({call, Ln, - {remote, _, {atom, _, generic}, {atom, _, ldecode}}, - [{record, _, Type, _}] -}) -> - cc_decode(Ln, labelled_to, Type); - hook_generic({call, Ln, {remote, _, {atom, _, generic}, {atom, _, from}}, [{record, _, {var, _, _} = Struct, Type, _}] }) -> - {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, from}}, - [ - {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, - expr(Struct) - ] - }; + cc_from(Ln, from, Type, Struct); hook_generic({call, Ln, {remote, _, {atom, _, generic}, {atom, _, from}}, [{record, _, Type, _} = Struct] }) -> - {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, from}}, - [ - {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, - expr(Struct) - ] - }; + cc_from(Ln, from, Type, Struct); hook_generic({call, Ln, {remote, _, {atom, _, generic}, {atom, _, from}}, [{cons, Ln, {record, _, Type, _}, _} = Struct] +}) -> + cc_from(Ln, from, Type, Struct); + +hook_generic({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, from}}, + [{cons, Ln, {record, _, {var, _, _}, Type, _}, _} = Struct] +}) -> + cc_from(Ln, from, Type, Struct); + +hook_generic({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, Type}}, + [Struct] }) -> {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, from}}, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, to}}, [ + {atom, Ln, Type}, {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, expr(Struct) ] }; + hook_generic({call, Ln, - {remote, _, {atom, _, generic}, {atom, _, Type}}, + {remote, _, {atom, _, labelled}, {atom, _, encode}}, + [{record, _, Type, _}] +}) -> + cc_encode(Ln, labelled_from, Type); + +hook_generic({call, Ln, + {remote, _, {atom, _, labelled}, {atom, _, decode}}, + [{record, _, Type, _}] +}) -> + cc_decode(Ln, labelled_to, Type); + +hook_generic({call, Ln, + {remote, _, {atom, _, labelled}, {atom, _, from}}, + [{record, _, {var, _, _} = Struct, Type, _}] +}) -> + cc_from(Ln, labelled_from, Type, Struct); + +hook_generic({call, Ln, + {remote, _, {atom, _, labelled}, {atom, _, from}}, + [{record, _, Type, _} = Struct] +}) -> + cc_from(Ln, labelled_from, Type, Struct); + +hook_generic({call, Ln, + {remote, _, {atom, _, labelled}, {atom, _, from}}, + [{cons, Ln, {record, _, Type, _}, _} = Struct] +}) -> + cc_from(Ln, labelled_from, Type, Struct); + +hook_generic({call, Ln, + {remote, _, {atom, _, labelled}, {atom, _, from}}, + [{cons, Ln, {record, _, {var, _, _}, Type, _}, _} = Struct] +}) -> + cc_from(Ln, labelled_from, Type, Struct); + +hook_generic({call, Ln, + {remote, _, {atom, _, labelled}, {atom, _, Type}}, [Struct] }) -> {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, to}}, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, labelled_to}}, [ {atom, Ln, Type}, {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, @@ -209,6 +235,15 @@ cc_decode(Ln, With, Type) -> ]} }. +cc_from(Ln, With, Type, Struct) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, With}}, + [ + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + expr(Struct) + ] + }. + %%%------------------------------------------------------------------ %%% %%% stdlib-3.0/examples/erl_id_trans.erl diff --git a/test/generic_SUITE.erl b/test/generic_SUITE.erl index d9b4626..3e31e0e 100644 --- a/test/generic_SUITE.erl +++ b/test/generic_SUITE.erl @@ -25,6 +25,7 @@ syntax/1 , generic/1 , derived/1 +, labelled/1 , derived_labelled/1 ]). @@ -47,6 +48,14 @@ syntax(_) -> ok = transform("generic:adt(X)."), ok = transform("generic:encode(#adt{})."), ok = transform("generic:decode(#adt{})."), + + ok = transform("labelled:from(#adt{a = 1})."), + ok = transform("labelled:from(X#adt{})."), + ok = transform("labelled:adt(#{a => 1})."), + ok = transform("labelled:adt(X)."), + ok = transform("labelled:encode(#adt{})."), + ok = transform("labelled:decode(#adt{})."), + ok = transform("a:b(X)."). %% @@ -54,12 +63,11 @@ syntax(_) -> generic(_) -> Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, Expect = #{a => 1, b => <<"test">>, c => 2.0}, - Structs = [Struct], Expect = generic:from(Struct#adt{}), - [Expect] = generic:from(Structs#adt{}), + [Expect] = generic:from([Struct#adt{}]), Struct = generic:adt(generic:from(Struct#adt{})), - Structs = generic:adt(generic:from(Structs#adt{})). + [Struct] = generic:adt(generic:from([Struct#adt{}])). %% %% @@ -78,12 +86,22 @@ derived(_) -> %% %% +labelled(_) -> + Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, + Expect = #{<<"a">> => 1, <<"b">> => <<"test">>, <<"c">> => 2.0}, + + Expect = labelled:from(Struct#adt{}), + [Expect] = labelled:from([Struct#adt{}]), + Struct = labelled:adt(labelled:from(Struct#adt{})), + [Struct] = labelled:adt(labelled:from([Struct#adt{}])). +%% +%% derived_labelled(_) -> Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, Expect = #{<<"a">> => 1, <<"b">> => <<"test">>, <<"c">> => 2.0}, - Encode = generic:lencode(#adt{}), - Decode = generic:ldecode(#adt{}), + Encode = labelled:encode(#adt{}), + Decode = labelled:decode(#adt{}), Expect = Encode(Struct), [Expect] = Encode([Struct]), From 2b1e35d44270ef4c58b8520a9eaa8402f1996857 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Sun, 19 May 2019 17:15:30 +0300 Subject: [PATCH 12/18] use explicit syntax of type indication Usage of tags generic:from(X#type{}) is akward. A new proposal generic_of:type(X) is compatible with typical syntax of Erlang. --- doc/features.md | 4 +- src/generic.erl | 121 ++++++++++++++--------------------------- test/generic_SUITE.erl | 32 +++++------ 3 files changed, 59 insertions(+), 98 deletions(-) diff --git a/doc/features.md b/doc/features.md index 1db9d40..20ed0be 100644 --- a/doc/features.md +++ b/doc/features.md @@ -221,7 +221,7 @@ The library introduces a `generic` parse transform that implements automatic map example() -> %% %% builds a generic representation from #person{} ADT - Gen = generic:from(#person{ + Gen = generic_of:person(#person{ name = "Verner Pleishner", address = "Blumenstrasse 14", city = "Berne" @@ -229,7 +229,7 @@ example() -> %% %% builds #person{} ADT from generic representation - generic:person(Gen). + generic_to:person(Gen). ``` In facts, a macro magic happens behind to execute transformation without boilerplate. As the result a labeled generic representation is returned. diff --git a/src/generic.erl b/src/generic.erl index 44f8472..c25868c 100644 --- a/src/generic.erl +++ b/src/generic.erl @@ -4,9 +4,9 @@ -module(generic). -export([ - from/2 -, to/3 -, labelled_from/2 + generic_of/2 +, generic_to/3 +, labelled_of/2 , labelled_to/3 , parse_transform/2 ]). @@ -14,9 +14,9 @@ %% %% --spec from([atom()], tuple()) -> map(). +-spec generic_of([atom()], tuple()) -> map(). -from(Fields, Struct) +generic_of(Fields, Struct) when is_tuple(Struct) -> maps:from_list( [{Key, Value} || @@ -29,29 +29,29 @@ from(Fields, Struct) ] ); -from(Fields, Structs) +generic_of(Fields, Structs) when is_list(Structs) -> - [from(Fields, Struct) || Struct <- Structs]. + [generic_of(Fields, Struct) || Struct <- Structs]. %% %% --spec to(atom(), [atom()], map()) -> tuple(). +-spec generic_to(atom(), [atom()], map()) -> tuple(). -to(Type, Fields, Generic) +generic_to(Type, Fields, Generic) when is_map(Generic) -> list_to_tuple([Type | [maps:get(X, Generic, undefined) || X <- Fields]] ); -to(Type, Fields, Generics) +generic_to(Type, Fields, Generics) when is_list(Generics) -> - [to(Type, Fields, Generic) || Generic <- Generics]. + [generic_to(Type, Fields, Generic) || Generic <- Generics]. %% %% --spec labelled_from([atom()], tuple()) -> map(). +-spec labelled_of([atom()], tuple()) -> map(). -labelled_from(Fields, Struct) +labelled_of(Fields, Struct) when is_tuple(Struct) -> maps:from_list( [{typecast:s(Key), Value} || @@ -64,9 +64,9 @@ labelled_from(Fields, Struct) ] ); -labelled_from(Fields, Structs) +labelled_of(Fields, Structs) when is_list(Structs) -> - [labelled_from(Fields, Struct) || Struct <- Structs]. + [labelled_of(Fields, Struct) || Struct <- Structs]. %% %% @@ -92,57 +92,33 @@ hook_generic({call, Ln, {remote, _, {atom, _, generic}, {atom, _, encode}}, [{record, _, Type, _}] }) -> - cc_encode(Ln, from, Type); + cc_encode(Ln, generic_of, Type); hook_generic({call, Ln, {remote, _, {atom, _, generic}, {atom, _, decode}}, [{record, _, Type, _}] }) -> - cc_decode(Ln, to, Type); + cc_decode(Ln, generic_to, Type); hook_generic({call, Ln, - {remote, _, {atom, _, generic}, {atom, _, from}}, - [{record, _, {var, _, _} = Struct, Type, _}] -}) -> - cc_from(Ln, from, Type, Struct); - -hook_generic({call, Ln, - {remote, _, {atom, _, generic}, {atom, _, from}}, - [{record, _, Type, _} = Struct] -}) -> - cc_from(Ln, from, Type, Struct); - -hook_generic({call, Ln, - {remote, _, {atom, _, generic}, {atom, _, from}}, - [{cons, Ln, {record, _, Type, _}, _} = Struct] -}) -> - cc_from(Ln, from, Type, Struct); - -hook_generic({call, Ln, - {remote, _, {atom, _, generic}, {atom, _, from}}, - [{cons, Ln, {record, _, {var, _, _}, Type, _}, _} = Struct] + {remote, _, {atom, _, generic_of}, {atom, _, Type}}, + [Struct] }) -> - cc_from(Ln, from, Type, Struct); + cc_of(Ln, generic_of, Type, Struct); hook_generic({call, Ln, - {remote, _, {atom, _, generic}, {atom, _, Type}}, + {remote, _, {atom, _, generic_to}, {atom, _, Type}}, [Struct] }) -> - {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, to}}, - [ - {atom, Ln, Type}, - {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, - expr(Struct) - ] - }; - + cc_to(Ln, generic_to, Type, Struct); +%% +%% hook_generic({call, Ln, {remote, _, {atom, _, labelled}, {atom, _, encode}}, [{record, _, Type, _}] }) -> - cc_encode(Ln, labelled_from, Type); + cc_encode(Ln, labelled_of, Type); hook_generic({call, Ln, {remote, _, {atom, _, labelled}, {atom, _, decode}}, @@ -151,41 +127,16 @@ hook_generic({call, Ln, cc_decode(Ln, labelled_to, Type); hook_generic({call, Ln, - {remote, _, {atom, _, labelled}, {atom, _, from}}, - [{record, _, {var, _, _} = Struct, Type, _}] -}) -> - cc_from(Ln, labelled_from, Type, Struct); - -hook_generic({call, Ln, - {remote, _, {atom, _, labelled}, {atom, _, from}}, - [{record, _, Type, _} = Struct] -}) -> - cc_from(Ln, labelled_from, Type, Struct); - -hook_generic({call, Ln, - {remote, _, {atom, _, labelled}, {atom, _, from}}, - [{cons, Ln, {record, _, Type, _}, _} = Struct] -}) -> - cc_from(Ln, labelled_from, Type, Struct); - -hook_generic({call, Ln, - {remote, _, {atom, _, labelled}, {atom, _, from}}, - [{cons, Ln, {record, _, {var, _, _}, Type, _}, _} = Struct] + {remote, _, {atom, _, labelled_of}, {atom, _, Type}}, + [Struct] }) -> - cc_from(Ln, labelled_from, Type, Struct); + cc_of(Ln, labelled_of, Type, Struct); hook_generic({call, Ln, - {remote, _, {atom, _, labelled}, {atom, _, Type}}, + {remote, _, {atom, _, labelled_to}, {atom, _, Type}}, [Struct] }) -> - {call, Ln, - {remote, Ln, {atom, Ln, generic}, {atom, Ln, labelled_to}}, - [ - {atom, Ln, Type}, - {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, - expr(Struct) - ] - }; + cc_to(Ln, labelled_to, Type, Struct); hook_generic({call,Line,F0,As0}) -> %% N.B. If F an atom then call to local function or BIF, if F a @@ -235,7 +186,7 @@ cc_decode(Ln, With, Type) -> ]} }. -cc_from(Ln, With, Type, Struct) -> +cc_of(Ln, With, Type, Struct) -> {call, Ln, {remote, Ln, {atom, Ln, generic}, {atom, Ln, With}}, [ @@ -244,6 +195,16 @@ cc_from(Ln, With, Type, Struct) -> ] }. +cc_to(Ln, With, Type, Struct) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, With}}, + [ + {atom, Ln, Type}, + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]}, + expr(Struct) + ] + }. + %%%------------------------------------------------------------------ %%% %%% stdlib-3.0/examples/erl_id_trans.erl diff --git a/test/generic_SUITE.erl b/test/generic_SUITE.erl index 3e31e0e..a41182d 100644 --- a/test/generic_SUITE.erl +++ b/test/generic_SUITE.erl @@ -42,17 +42,17 @@ all() -> %% %% syntax(_) -> - ok = transform("generic:from(#adt{a = 1})."), - ok = transform("generic:from(X#adt{})."), - ok = transform("generic:adt(#{a => 1})."), - ok = transform("generic:adt(X)."), + ok = transform("generic_of:adt(#adt{a = 1})."), + ok = transform("generic_of:adt(X)."), + ok = transform("generic_to:adt(#{a => 1})."), + ok = transform("generic_to:adt(X)."), ok = transform("generic:encode(#adt{})."), ok = transform("generic:decode(#adt{})."), - ok = transform("labelled:from(#adt{a = 1})."), - ok = transform("labelled:from(X#adt{})."), - ok = transform("labelled:adt(#{a => 1})."), - ok = transform("labelled:adt(X)."), + ok = transform("labelled_of:adt(#adt{a = 1})."), + ok = transform("labelled_of:adt(X)."), + ok = transform("labelled_to:adt(#{a => 1})."), + ok = transform("labelled_to:adt(X)."), ok = transform("labelled:encode(#adt{})."), ok = transform("labelled:decode(#adt{})."), @@ -64,10 +64,10 @@ generic(_) -> Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, Expect = #{a => 1, b => <<"test">>, c => 2.0}, - Expect = generic:from(Struct#adt{}), - [Expect] = generic:from([Struct#adt{}]), - Struct = generic:adt(generic:from(Struct#adt{})), - [Struct] = generic:adt(generic:from([Struct#adt{}])). + Expect = generic_of:adt(Struct), + [Expect] = generic_of:adt([Struct]), + Struct = generic_to:adt(generic_of:adt(Struct)), + [Struct] = generic_to:adt(generic_of:adt([Struct])). %% %% @@ -90,10 +90,10 @@ labelled(_) -> Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, Expect = #{<<"a">> => 1, <<"b">> => <<"test">>, <<"c">> => 2.0}, - Expect = labelled:from(Struct#adt{}), - [Expect] = labelled:from([Struct#adt{}]), - Struct = labelled:adt(labelled:from(Struct#adt{})), - [Struct] = labelled:adt(labelled:from([Struct#adt{}])). + Expect = labelled_of:adt(Struct), + [Expect] = labelled_of:adt([Struct]), + Struct = labelled_to:adt(labelled_of:adt(Struct)), + [Struct] = labelled_to:adt(labelled_of:adt([Struct])). %% %% derived_labelled(_) -> From 557984ef01bafe3833f9d52ba86b9d791520467d Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Sat, 25 May 2019 21:55:04 +0300 Subject: [PATCH 13/18] Support custom layout for generics --- src/generic.erl | 47 ++++++++++++++++++++++++++++++++++++++++++ test/generic_SUITE.erl | 27 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/src/generic.erl b/src/generic.erl index c25868c..c92aa5a 100644 --- a/src/generic.erl +++ b/src/generic.erl @@ -106,12 +106,25 @@ hook_generic({call, Ln, }) -> cc_of(Ln, generic_of, Type, Struct); +hook_generic({call, Ln, + {remote, _, {atom, _, generic_of}, {atom, _, _}}, + [Spec, Struct] +}) -> + cc_explicit_of(Ln, generic_of, Spec, Struct); + hook_generic({call, Ln, {remote, _, {atom, _, generic_to}, {atom, _, Type}}, [Struct] }) -> cc_to(Ln, generic_to, Type, Struct); +hook_generic({call, Ln, + {remote, _, {atom, _, generic_to}, {atom, _, Type}}, + [Spec, Struct] +}) -> + cc_explicit_to(Ln, generic_to, Type, Spec, Struct); + + %% %% hook_generic({call, Ln, @@ -132,12 +145,24 @@ hook_generic({call, Ln, }) -> cc_of(Ln, labelled_of, Type, Struct); +hook_generic({call, Ln, + {remote, _, {atom, _, labelled_of}, {atom, _, _}}, + [Spec, Struct] +}) -> + cc_explicit_of(Ln, labelled_of, Spec, Struct); + hook_generic({call, Ln, {remote, _, {atom, _, labelled_to}, {atom, _, Type}}, [Struct] }) -> cc_to(Ln, labelled_to, Type, Struct); +hook_generic({call, Ln, + {remote, _, {atom, _, labelled_to}, {atom, _, Type}}, + [Spec, Struct] +}) -> + cc_explicit_to(Ln, labelled_to, Type, Spec, Struct); + hook_generic({call,Line,F0,As0}) -> %% N.B. If F an atom then call to local function or BIF, if F a %% remote structure (see below) then call to other module, @@ -195,6 +220,16 @@ cc_of(Ln, With, Type, Struct) -> ] }. +cc_explicit_of(Ln, With, Spec, Struct) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, With}}, + [ + Spec, + expr(Struct) + ] + }. + + cc_to(Ln, With, Type, Struct) -> {call, Ln, {remote, Ln, {atom, Ln, generic}, {atom, Ln, With}}, @@ -205,6 +240,18 @@ cc_to(Ln, With, Type, Struct) -> ] }. +cc_explicit_to(Ln, With, Type, Spec, Struct) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, With}}, + [ + {atom, Ln, Type}, + Spec, + expr(Struct) + ] + }. + + + %%%------------------------------------------------------------------ %%% %%% stdlib-3.0/examples/erl_id_trans.erl diff --git a/test/generic_SUITE.erl b/test/generic_SUITE.erl index a41182d..7db441d 100644 --- a/test/generic_SUITE.erl +++ b/test/generic_SUITE.erl @@ -24,8 +24,10 @@ -export([ syntax/1 , generic/1 +, generic_explicit_spec/1 , derived/1 , labelled/1 +, labelled_explicit_spec/1 , derived_labelled/1 ]). @@ -69,6 +71,18 @@ generic(_) -> Struct = generic_to:adt(generic_of:adt(Struct)), [Struct] = generic_to:adt(generic_of:adt([Struct])). +%% +%% +generic_explicit_spec(_) -> + Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, + Expect = #{x => 1, y => <<"test">>, z => 2.0}, + Spec = [x, y, z], + + Expect = generic_of:adt(Spec, Struct), + [Expect] = generic_of:adt(Spec, [Struct]), + Struct = generic_to:adt(Spec, generic_of:adt(Spec, Struct)), + [Struct] = generic_to:adt(Spec, generic_of:adt(Spec, [Struct])). + %% %% derived(_) -> @@ -94,6 +108,19 @@ labelled(_) -> [Expect] = labelled_of:adt([Struct]), Struct = labelled_to:adt(labelled_of:adt(Struct)), [Struct] = labelled_to:adt(labelled_of:adt([Struct])). + +%% +%% +labelled_explicit_spec(_) -> + Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, + Expect = #{<<"x">> => 1, <<"y">> => <<"test">>, <<"z">> => 2.0}, + Spec = [x, y, z], + + Expect = labelled_of:adt(Spec, Struct), + [Expect] = labelled_of:adt(Spec, [Struct]), + Struct = labelled_to:adt(Spec, labelled_of:adt(Spec, Struct)), + [Struct] = labelled_to:adt(Spec, labelled_of:adt(Spec, [Struct])). + %% %% derived_labelled(_) -> From 7048e0d1dc7c757224b8b98b53e2cea7557b4ffe Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Thu, 30 May 2019 14:04:37 +0300 Subject: [PATCH 14/18] Update documentation about generic --- CHANGELOG.md | 11 ++ LICENSE | 2 +- README.md | 17 +- doc/cheatsheet.md | 31 ---- doc/features.md | 8 +- doc/generic.md | 243 +++++++++++++++++++++++++++ examples/Makefile | 13 +- examples/rebar.config | 9 +- examples/src/examples_generic.erl | 118 +++++++++++++ examples/src/examples_generic_io.erl | 75 +++++++++ examples/test/tests.config | 1 + src/generic.erl | 123 ++++++++++++++ test/generic_SUITE.erl | 99 ++++++++++- 13 files changed, 680 insertions(+), 70 deletions(-) delete mode 100644 doc/cheatsheet.md create mode 100644 doc/generic.md create mode 100644 examples/src/examples_generic.erl create mode 100644 examples/src/examples_generic_io.erl create mode 100644 examples/test/tests.config diff --git a/CHANGELOG.md b/CHANGELOG.md index dd43cab..edbc7b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ The library uses [semantic versions](http://semver.org) to identify stable releases. +## Release 4.6.x + +**Features** +* [#68](https://github.com/fogfish/datum/issues/68): Implements generic representations + +## Release 4.5.x + +**Features** +* [#54](https://github.com/fogfish/datum/issues/54): Implements pattern matching within do-notation + + ## Release 4.4.0 **Features** diff --git a/LICENSE b/LICENSE index d645695..ca806d8 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2012 Dmitry Kolesnikov Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 42dfd52..bd89058 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,15 @@ The [feature overview](doc/features.md) provides an introduction to datum features, use-cases and reasoning of they existence: -* notation for `option` and `either` types -* data structures with common behavior: **foldable**, **traversable** and **map-like**. +* `option` and `either` type notations +* a set of generic data types that can be inspected, traversed, and manipulated with common behavior: **foldable**, **traversable** and **map-like**. * pure functional **data types**: binary search tree, red-black tree, heap, queues, and others * **streams** or lazy lists are a sequential data structure that contains on demand computed elements. * resembles concept of getters and setters ([**lens**](doc/lens.md)) for complex algebraic data types. -* define a **category pattern**, **monads** and they composition for Erlang applications -* **do-notation** with pattern matching +* mapping of algebraic data types to they [**generic**](doc/generic.md) representation and back +* define a [**category pattern**](doc/category.md), [**monads**](doc/monad.md) and they composition for Erlang applications. You might be familiar with this concept as pipe, flow or function composition. +* generic [**do-notation**](doc/monad.md) with pattern matching. * [**typecasts**](doc/typecast.md) of primitive data types -* mapping of algebraic data types to they **generic** representation and back * supports **OTP/18.x** or later release @@ -91,9 +91,4 @@ If you experience any issues with the library, please let us know via [GitHub is ## License -Copyright 2012 Dmitry Kolesnikov - -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - +[![See LICENSE](https://img.shields.io/hexpm/l/plug.svg?style=for-the-badge)](LICENSE) diff --git a/doc/cheatsheet.md b/doc/cheatsheet.md deleted file mode 100644 index 4126b60..0000000 --- a/doc/cheatsheet.md +++ /dev/null @@ -1,31 +0,0 @@ -# Datum Cheat sheet - -* [Lens](#lens) - - -## Lens - -Focus on element with - -Data type | Lens ---- | --- -List | `lens:hd()`, `lens:tl()` -Tuple | `lens:t1()`, `lens:t2()`, `lens:t3()`, `lens:ti(Int)` -Map | `lens:at(Key)` -KeyList | `lens:keylist(Int, Key)` -PropList | `lens:pair(Key)` - -Lens support **composition** of lenses with `lens:c(...)` and construction of **product** lens `lens:p(...)` - - -Use lenses to - -Intent | Interface ---- | --- -Get | `lens:get/2` -Put | `lens:put/3` -Update | `lens:map/3` -Transform structure | `lens:iso/4` - - - diff --git a/doc/features.md b/doc/features.md index 20ed0be..5c72fb2 100644 --- a/doc/features.md +++ b/doc/features.md @@ -8,7 +8,7 @@ * [Pure functional data types](#pure-functional-data-types) * [Stream](#stream) * [Lens](#lens) -* [Generic](#generic) +* [Generic representation](#generic-representation) * [Category pattern](#category-pattern) * [Monad](#monad) @@ -205,9 +205,9 @@ end. lens:get(Lens(a), lens:put(Lens(a), 1, bst:new())). ``` -## Generic +## Generic representation -Automatic transformation of algebraic data types to they generic representation solve wide problems of generic programming. Erlang has strong foundation and language primitives on this aspect due to ducked-typed nature. Existence of generic types (lists, maps and tuples) makes a programming task trivial. However, absence of strong types makes it error prone while switching a context from generic algorithms to business domain. Thus usage of algebraic data types (encoded in Erlang records) improves maintainability of domain specific code. +Automatic transformation of algebraic data types to they generic representation solve wide problems of generic programming. Erlang has strong foundation and language primitives on this aspect due to dynamically typed nature. Existence of generic types (lists, maps and tuples) makes a programming task trivial. However, absence of strong types makes it error prone while switching a context from generic algorithms to business domain. Thus usage of algebraic data types (encoded in Erlang records) improves maintainability of domain specific code. > The main advantage of using records rather than tuples is that fields in a record are accessed by name, whereas fields in a tuple are accessed by position. @@ -234,6 +234,8 @@ example() -> In facts, a macro magic happens behind to execute transformation without boilerplate. As the result a labeled generic representation is returned. +See details about [generic patterns](generic.md). + ## Category pattern diff --git a/doc/generic.md b/doc/generic.md new file mode 100644 index 0000000..474caee --- /dev/null +++ b/doc/generic.md @@ -0,0 +1,243 @@ +# Generic representation + +> Generic programming is a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters. -- said by [Wikipedia](https://en.wikipedia.org/wiki/Generic_programming). + +Erlang permits writing a generic code. It is known to be dynamic, strong typing language, the article [Types (or lack thereof)](https://learnyousomeerlang.com/types-or-lack-thereof) describes it in excellent manner. The in-depth study about type systems have been presented at [Point Of No Local Return: The Continuing Story Of Erlang Type Systems](http://www.erlang-factory.com/static/upload/media/1465548492405302zeeshanlakhanipointofnolocalreturn.pdf). + +Type theory and a functional programming also operates with [algebraic data types](https://en.wikipedia.org/wiki/Algebraic_data_type). They are known as a composition of other types. The theory defines two classes of compositions: product types (tuples, records) and co-product types (sum, enumeration or variant types). Product types are strongly expressed by [records](http://erlang.org/doc/reference_manual/records.html) in Erlang; co-products are loosely defined (we skip them at current release). + +```erlang +-type fullname() :: binary(). +-type address() :: binary(). +-type city() :: binary(). + +-record(person, { + name :: fullname(), + address :: address(), + city :: city() +}). +``` + +The beauty of Erlang records (product type) is that they definitions are only available at compile time. The compiler has complete knowledge of defined "algebra" and catches misuse errors. The usage of records in your code benefits to write correct, maintainable code and support refactoring. Use them to define your domain models! + +```erlang +#person{birthday = "18810509"}. + +%% Compiling src/person.erl failed +%% src/person.erl:18: field birthday undefined in record person +``` + +## Generic programming with records + +Erlang offers few features that helps with generic programming: + +1. record expressions are translated to tuple by compiler, use `element/2` and `setelement/3` for generic access to tuple elements. +2. functions `tuple_to_list/1` and `list_to_tuple/1` are transformers to another an alternative generic representation. +3. pseudo function `record_info/2` to obtain record structure. + +As an example, a typical macro here to cast record type to the map: + +```erlang +-define(encode(Type, Struct), + maps:from_list( + [{Key, Value} || + {Key, Value} <- lists:zip( + record_info(fields, Type), + tl(tuple_to_list(Struct)) + ), + Value /= undefined, + Value /= null + ] + ) +). +``` + +Unfortunately, usage of records have a couple of disadvantages that make them hardly usable in large projects: + +* external serialization of domain models often requires knowledge of internals about records: types, attributes names, cardinality, etc. This information is not implicitly available unless you import `.hrl` file with records definition. +* record transformations to other record or any external format requires boilerplate code. +* generic programming with records requires macros. +* new built-in data type `maps()` obsoletes usage of records for casual use-cases. + +For these reasons, we would like to improve records runtime flexibility through `parse_transform` while keeping its compile-time benefits for domain modeling. + +The latest release of library aims on most common tasks in software engineering: representation switching. + + +## Switching representations + +`datum` provides a `parse_transform` called `generic` that allows us to switch back and forth between a concrete records (ADT) and its generic representa􏰀on without boilerplate. Please not that the generic representation is opaque structure for your code. Please follow examples of generic [here](../examples/src/examples_generic.erl). + + + + +### Semi-auto derivation + +It is convenient to have just encoder/decoder. Semi-auto codec makes a magic of switching representation between different ADTs. + +```erlang +-compile({parse_transform, generic}). + +-record(person, {name, age, address}). + +person() -> + #person{ + name = "Verner Pleishner", + age = 64, + address = "Blumenstrasse 14, Berne, 3013" + }. + +semi_auto() -> + Person = person(), + Generic = generic_of:person(Person), + Employee = generic_to:person(Generic). +``` + +The generic representation carries-on instances of records fields and associated metadata, the current implementation uses `maps()` but this is subject to change in future release of the library. If two ADTs have the similar representation we can convert back and forth between them using their generics representation: + +```erlang +-compile({parse_transform, generic}). + +-record(person, {name, age, address}). +-record(employee, {name, age, address}). + +Generic = generic_of:person(#person{ ... }). +Employee = generic_to:employee(Generic). +``` + +Please note that "similar" representation means -- set of common fields. + +```erlang +-compile({parse_transform, generic}). + +-record(person, {name, age, address}). +-record(visitor, {name, age}). + +Generic = generic_of:person(#person{ ... }). +Visitor = generic_to:visitor(Generic). +``` + +Semi-auto derivation works with recursive types but current version supports only lists. + +```erlang +-spec fetch_list_of_persons() -> [#person{}]. + +Generic = generic_of:person( fetch_list_of_persions() ). +Employee = generic_to:employee(Generic). +``` + + +### Assisted derivation + +It is also possible to customize encoders/decoders for records without generic derivation. The feature supports morphism if two ADTs do not share similar representation. + +```erlang +-compile({parse_transform, generic}). + +-record(person, {name, age, address}). +-record(estate, {location, owner}). + +person() -> + #person{ + name = "Verner Pleishner", + age = 64, + address = "Blumenstrasse 14, Berne, 3013" + }. + +assisted_morphism() -> + Person = person(), + Generic = generic_of:person([name, age, address], Person), + Employee = generic_to:estate([address, name], Generic). +``` + + +### Partial application + +Partial application is a tool to make a generic processing for your data domain (e.g. generic input/output to database). The generic implements helper utilities that returns encoder/decoder as a function. + +```erlang +-compile({parse_transform, generic}). + +-record(person, {name, age, address}). + +Encoder = generic:encode(#person{}). +Decoder = generic:decode(#person{}). +``` + +Use this functions to switch representation of your records at runtime. + +```erlang +person() -> + #person{ + name = "Verner Pleishner", + age = 64, + address = "Blumenstrasse 14, Berne, 3013" + }. + +Person = person(). +Generic = Encoder(Person). +Person = Decoder(Generic). +``` + +You can pass these codec functions to any processes named after the records type. Then the implementation of these process needs to deal only with aspects of generic data processing. See its [example](../examples/src/examples_generic_io.erl). + +```erlang +{ok, _} = examples_generic_io:start_link(person, Encoder, Decoder). + +examples_generic_io:send(Person). +examples_generic_io:recv(#person{}). +``` + + +### Custom encoders/decoders + +Only flat data structures are supported at the moment. You still need to deal with custom derivation. There are few approaches here + +You can write encoder from scratch. In many cases semi-auto and assisted derivation helps you here -- you only writes a custom code for container types but you benefit of derived codec for atomic one. + +```erlang +-compile({parse_transform, generic}). + +-record(address, {street, city, zipcode}). +-record(person, {name, age, address}). +-record(estate, {location, owner}). + +custom_codec() -> + Person = person(), + Address = address(), + Generic = generic_of:estate(#estate{ + location = generic_of:address(Address), + owner = generic_of:person(Person) + }). +``` + +The custom decoder requires traversal through generic data structures. This task do not differs from traditional approach, which involves a portion of boilerplate. The library offers only a [lens abstraction](lens.md) to solve the problem in a relatively boilerplate-free way. + +```erlang + +custom_decoder() -> + ... + Lens = lens:p(#estate{ + location = lens:c(lens:at(location), generic:lens(#address{})), + owner = lens:c(lens:at(owner), generic:lens(#person{})) + }), + Estate = lens:get(Lens, Generic). + +``` + +Please note that lens isomorphism is an alternative abstraction that facilitates variety of transformation use-cases. You can implement custom codec just with lenses but you might observe that your code becomes verbose. + + +## Conclusion + +Generic programming is widely known technique, which is well supported by Erlang. This effort makes a convenient to convert specific types into generic ones that we can manipulate with common code. The defined technique has been used to implement RESTfull API and AWS DynamoDB serialization logic, while keeping domain model as Erlang records for purpose of maintainability and catches misuse errors at compile time. diff --git a/examples/Makefile b/examples/Makefile index 93c59d6..45e8779 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -1,14 +1,3 @@ -## -## @doc -## an example Makefile to build and ship erlang software -## -## APP - identity of the application -## ORG - identity of the organization -## URI - identity of the docker repository with last / - APP = examples -ORG = -URI = - -include ../erlang.mk +include ../beam.mk diff --git a/examples/rebar.config b/examples/rebar.config index 3c3a811..1fe5fef 100644 --- a/examples/rebar.config +++ b/examples/rebar.config @@ -1,9 +1,10 @@ {erl_opts, []}. {deps, [ - %% Note: use hex package for productions, git master unlock recent features - {datum, ".*", - {git, "https://github.com/fogfish/datum.git", {branch, master}} - } + {datum, {path, "../"}} +]}. + +{plugins, [ + rebar3_path_deps ]}. diff --git a/examples/src/examples_generic.erl b/examples/src/examples_generic.erl new file mode 100644 index 0000000..4071fff --- /dev/null +++ b/examples/src/examples_generic.erl @@ -0,0 +1,118 @@ +%% +%% Copyright (c) 2018, Dmitry Kolesnikov +%% All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% @doc +%% Examples of Generic usage in Erlang +-module(examples_generic). +-compile({parse_transform, generic}). + +-compile(export_all). + +-record(address, { + street = undefined :: string(), + city = undefined :: string(), + zipcode = undefined :: string() +}). + +-record(person, { + name = undefined :: string(), + age = undefined :: integer(), + address = undefined :: string() +}). + +-record(employee, { + name = undefined :: string(), + age = undefined :: integer(), + address = undefined :: string() +}). + +-record(estate, { + location = undefined :: string() | #address{}, + owner = undefined :: string() | #person{} +}). + +person() -> + #person{ + name = "Verner Pleishner", + age = 64, + address = "Blumenstrasse 14, Berne, 3013" + }. + +address() -> + #address{ + street = "Blumenstrasse 14", + city = "Berne", + zipcode = "3013" + }. + +%% +%% +semi_auto() -> + Person = person(), + Generic = generic_of:person(Person), + Person = generic_to:person(Generic). + +semi_auto_morphism() -> + Person = person(), + Generic = generic_of:person(Person), + _Employee = generic_to:employee(Generic). + +semi_auto_recursive_type_list() -> + Persons = [person() || _ <- lists:seq(1, 10)], + Generic = generic_of:person(Persons), + _Employees = generic_to:employee(Generic). + +%% +%% +assisted_morphism() -> + Person = person(), + Generic = generic_of:person([name, age, address], Person), + _Employee = generic_to:estate([address, name], Generic). + +%% +%% +partial_application() -> + Encoder = generic:encode(#person{}), + Decoder = generic:decode(#person{}), + + Person = person(), + Generic = Encoder(Person), + Person = Decoder(Generic). + +generic_process() -> + Encoder = generic:encode(#person{}), + Decoder = generic:decode(#person{}), + {ok, _} = examples_generic_io:start_link(person, Encoder, Decoder), + + Person = person(), + ok = examples_generic_io:send(Person), + {ok, Person} = examples_generic_io:recv(#person{}). + +%% +%% +custom_codecs() -> + Person = person(), + Address = address(), + Generic = generic_of:estate(#estate{ + location = generic_of:address(Address), + owner = generic_of:person(Person) + }), + Lens = lens:p(#estate{ + location = lens:c(lens:at(location), generic:lens(#address{})), + owner = lens:c(lens:at(owner), generic:lens(#person{})) + }), + _Estate = lens:get(Lens, Generic). + diff --git a/examples/src/examples_generic_io.erl b/examples/src/examples_generic_io.erl new file mode 100644 index 0000000..3140311 --- /dev/null +++ b/examples/src/examples_generic_io.erl @@ -0,0 +1,75 @@ +%% +%% Copyright (c) 2018, Dmitry Kolesnikov +%% All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% @doc +%% Examples of Generic usage in Erlang +-module(examples_generic_io). +-behaviour(gen_server). + +-export([ + send/1 +, recv/1 +, start_link/3 +, init/1 +, terminate/2 +, handle_call/3 +, handle_cast/2 +, handle_info/2 +, code_change/3 +]). + +-record(state, {encoder, decoder, generic}). + +%% +%% +send(Struct) when is_tuple(Struct) -> + gen_server:call(erlang:element(1, Struct), {send, Struct}). + +%% +%% +recv(Struct) when is_tuple(Struct) -> + gen_server:call(erlang:element(1, Struct), {recv, Struct}). + +%% +%% +start_link(Type, Encoder, Decoder) -> + gen_server:start_link({local, Type}, ?MODULE, [Encoder, Decoder], []). + +init([Encoder, Decoder]) -> + {ok, #state{encoder = Encoder, decoder = Decoder}}. + +terminate(_, _) -> + ok. + +handle_call({send, Struct}, _, #state{encoder = Encoder} = State) -> + {reply, ok, + State#state{ + generic = Encoder(Struct) + } + }; + +handle_call({recv, _}, _, #state{decoder = Decoder, generic = Generic} = State) -> + {reply, {ok, Decoder(Generic)}, State}. + +handle_cast(_, State) -> + {noreply, State}. + +handle_info(_, State) -> + {noreply, State}. + +code_change(_, State, _) -> + {ok, State}. + diff --git a/examples/test/tests.config b/examples/test/tests.config new file mode 100644 index 0000000..55c6084 --- /dev/null +++ b/examples/test/tests.config @@ -0,0 +1 @@ +{suites, ".", all}. diff --git a/src/generic.erl b/src/generic.erl index c92aa5a..2be056c 100644 --- a/src/generic.erl +++ b/src/generic.erl @@ -6,8 +6,10 @@ -export([ generic_of/2 , generic_to/3 +, generic_lens/2 , labelled_of/2 , labelled_to/3 +, labelled_lens/2 , parse_transform/2 ]). @@ -47,6 +49,15 @@ generic_to(Type, Fields, Generics) when is_list(Generics) -> [generic_to(Type, Fields, Generic) || Generic <- Generics]. +%% +%% +-spec generic_lens(atom(), [atom()]) -> datum:lens(). + +generic_lens(Type, Fields) -> + lens:p(list_to_tuple([Type | + [lens:at(X) || X <- Fields]] + )). + %% %% -spec labelled_of([atom()], tuple()) -> map(). @@ -82,6 +93,15 @@ labelled_to(Type, Fields, Generics) when is_list(Generics) -> [labelled_to(Type, Fields, Generic) || Generic <- Generics]. +%% +%% +-spec labelled_lens(atom(), [atom()]) -> datum:lens(). + +labelled_lens(Type, Fields) -> + lens:p(list_to_tuple([Type | + [lens:at(typecast:s(X)) || X <- Fields]] + )). + %% %% -spec hook_generic(_) -> _. @@ -94,12 +114,36 @@ hook_generic({call, Ln, }) -> cc_encode(Ln, generic_of, Type); +hook_generic({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, encode}}, + [Spec, {record, _, Type, _}] +}) -> + cc_encode(Ln, generic_of, Spec, Type); + hook_generic({call, Ln, {remote, _, {atom, _, generic}, {atom, _, decode}}, [{record, _, Type, _}] }) -> cc_decode(Ln, generic_to, Type); +hook_generic({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, decode}}, + [Spec, {record, _, Type, _}] +}) -> + cc_decode(Ln, generic_to, Spec, Type); + +hook_generic({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, lens}}, + [{record, _, Type, _}] +}) -> + cc_lens(Ln, generic_lens, Type); + +hook_generic({call, Ln, + {remote, _, {atom, _, generic}, {atom, _, lens}}, + [Spec, {record, _, Type, _}] +}) -> + cc_lens(Ln, generic_lens, Spec, Type); + hook_generic({call, Ln, {remote, _, {atom, _, generic_of}, {atom, _, Type}}, [Struct] @@ -133,12 +177,36 @@ hook_generic({call, Ln, }) -> cc_encode(Ln, labelled_of, Type); +hook_generic({call, Ln, + {remote, _, {atom, _, labelled}, {atom, _, encode}}, + [Spec, {record, _, Type, _}] +}) -> + cc_encode(Ln, labelled_of, Spec, Type); + hook_generic({call, Ln, {remote, _, {atom, _, labelled}, {atom, _, decode}}, [{record, _, Type, _}] }) -> cc_decode(Ln, labelled_to, Type); +hook_generic({call, Ln, + {remote, _, {atom, _, labelled}, {atom, _, decode}}, + [Spec, {record, _, Type, _}] +}) -> + cc_decode(Ln, labelled_to, Spec, Type); + +hook_generic({call, Ln, + {remote, _, {atom, _, labelled}, {atom, _, lens}}, + [{record, _, Type, _}] +}) -> + cc_lens(Ln, labelled_lens, Type); + +hook_generic({call, Ln, + {remote, _, {atom, _, labelled}, {atom, _, lens}}, + [Spec, {record, _, Type, _}] +}) -> + cc_lens(Ln, labelled_lens, Spec, Type); + hook_generic({call, Ln, {remote, _, {atom, _, labelled_of}, {atom, _, Type}}, [Struct] @@ -191,6 +259,25 @@ cc_encode(Ln, With, Type) -> ]} }. +cc_encode(Ln, With, Spec, _Type) -> + {'fun', Ln, + {clauses, [ + {clause, Ln, + [{var, Ln, 'X'}], + [], + [ + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, With}}, + [ + Spec, + {var, Ln, 'X'} + ] + } + ] + } + ]} + }. + cc_decode(Ln, With, Type) -> {'fun', Ln, {clauses, [ @@ -211,6 +298,26 @@ cc_decode(Ln, With, Type) -> ]} }. +cc_decode(Ln, With, Spec, Type) -> + {'fun', Ln, + {clauses, [ + {clause, Ln, + [{var, Ln, 'X'}], + [], + [ + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, With}}, + [ + {atom, Ln, Type}, + Spec, + {var, Ln, 'X'} + ] + } + ] + } + ]} + }. + cc_of(Ln, With, Type, Struct) -> {call, Ln, {remote, Ln, {atom, Ln, generic}, {atom, Ln, With}}, @@ -250,7 +357,23 @@ cc_explicit_to(Ln, With, Type, Spec, Struct) -> ] }. +cc_lens(Ln, With, Type) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, With}}, + [ + {atom, Ln, Type}, + {call, Ln, {atom, Ln, record_info}, [{atom, Ln, fields}, {atom, Ln, Type}]} + ] + }. +cc_lens(Ln, With, Spec, Type) -> + {call, Ln, + {remote, Ln, {atom, Ln, generic}, {atom, Ln, With}}, + [ + {atom, Ln, Type}, + Spec + ] + }. %%%------------------------------------------------------------------ %%% diff --git a/test/generic_SUITE.erl b/test/generic_SUITE.erl index 7db441d..e3f37a6 100644 --- a/test/generic_SUITE.erl +++ b/test/generic_SUITE.erl @@ -24,11 +24,17 @@ -export([ syntax/1 , generic/1 -, generic_explicit_spec/1 -, derived/1 +, generic_assisted/1 +, generic_partial/1 +, generic_partial_assisted/1 +, generic_lens/1 +, generic_lens_assisted/1 , labelled/1 -, labelled_explicit_spec/1 -, derived_labelled/1 +, labelled_assisted/1 +, labelled_partial/1 +, labelled_partial_assisted/1 +, labelled_lens/1 +, labelled_lens_assisted/1 ]). -record(adt, {a, b, c}). @@ -73,7 +79,7 @@ generic(_) -> %% %% -generic_explicit_spec(_) -> +generic_assisted(_) -> Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, Expect = #{x => 1, y => <<"test">>, z => 2.0}, Spec = [x, y, z], @@ -85,7 +91,7 @@ generic_explicit_spec(_) -> %% %% -derived(_) -> +generic_partial(_) -> Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, Expect = #{a => 1, b => <<"test">>, c => 2.0}, @@ -98,6 +104,45 @@ derived(_) -> Struct = Decode(Encode(Struct)), [Struct] = Decode(Encode([Struct])). +%% +%% +generic_partial_assisted(_) -> + Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, + Expect = #{x => 1, y => <<"test">>, z => 2.0}, + Spec = [x, y, z], + + Encode = generic:encode(Spec, #adt{}), + Decode = generic:decode(Spec, #adt{}), + + Expect = Encode(Struct), + [Expect] = Encode([Struct]), + + Struct = Decode(Encode(Struct)), + [Struct] = Decode(Encode([Struct])). + +%% +%% +generic_lens(_) -> + Struct = #{a => 1, b => <<"test">>, c => 2.0}, + Expect = #adt{a = 1, b = <<"test">>, c = 2.0}, + + Lens = generic:lens(#adt{}), + + Expect = lens:get(Lens, Struct), + [Expect] = lens:get(lens:c(lens:traverse(), Lens), [Struct]). + +%% +%% +generic_lens_assisted(_) -> + Struct = #{x => 1, y => <<"test">>, z => 2.0}, + Expect = #adt{a = 1, b = <<"test">>, c = 2.0}, + Spec = [x, y, z], + + Lens = generic:lens(Spec, #adt{}), + + Expect = lens:get(Lens, Struct), + [Expect] = lens:get(lens:c(lens:traverse(), Lens), [Struct]). + %% %% labelled(_) -> @@ -111,7 +156,7 @@ labelled(_) -> %% %% -labelled_explicit_spec(_) -> +labelled_assisted(_) -> Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, Expect = #{<<"x">> => 1, <<"y">> => <<"test">>, <<"z">> => 2.0}, Spec = [x, y, z], @@ -123,7 +168,7 @@ labelled_explicit_spec(_) -> %% %% -derived_labelled(_) -> +labelled_partial(_) -> Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, Expect = #{<<"a">> => 1, <<"b">> => <<"test">>, <<"c">> => 2.0}, @@ -136,6 +181,44 @@ derived_labelled(_) -> Struct = Decode(Encode(Struct)), [Struct] = Decode(Encode([Struct])). +%% +%% +labelled_partial_assisted(_) -> + Struct = #adt{a = 1, b = <<"test">>, c = 2.0}, + Expect = #{<<"x">> => 1, <<"y">> => <<"test">>, <<"z">> => 2.0}, + Spec = [x, y, z], + + Encode = labelled:encode(Spec, #adt{}), + Decode = labelled:decode(Spec, #adt{}), + + Expect = Encode(Struct), + [Expect] = Encode([Struct]), + + Struct = Decode(Encode(Struct)), + [Struct] = Decode(Encode([Struct])). + +%% +%% +labelled_lens(_) -> + Struct = #{<<"a">> => 1, <<"b">> => <<"test">>, <<"c">> => 2.0}, + Expect = #adt{a = 1, b = <<"test">>, c = 2.0}, + + Lens = labelled:lens(#adt{}), + + Expect = lens:get(Lens, Struct), + [Expect] = lens:get(lens:c(lens:traverse(), Lens), [Struct]). + +%% +%% +labelled_lens_assisted(_) -> + Struct = #{<<"x">> => 1, <<"y">> => <<"test">>, <<"z">> => 2.0}, + Expect = #adt{a = 1, b = <<"test">>, c = 2.0}, + Spec = [x, y, z], + + Lens = labelled:lens(Spec, #adt{}), + + Expect = lens:get(Lens, Struct), + [Expect] = lens:get(lens:c(lens:traverse(), Lens), [Struct]). %%%------------------------------------------------------------------ %%% From 664f3604bf7c71a4f703f7af10356a677c9b9da3 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Thu, 30 May 2019 14:16:10 +0300 Subject: [PATCH 15/18] Update Changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index edbc7b8..fd41646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,18 @@ The library uses [semantic versions](http://semver.org) to identify stable relea **Features** * [#68](https://github.com/fogfish/datum/issues/68): Implements generic representations + ## Release 4.5.x **Features** * [#54](https://github.com/fogfish/datum/issues/54): Implements pattern matching within do-notation +* Support product lenses for records (`lens:p/1`). +* Add typecast utility for scalar types. +* Support MFA generator for streams + +**Improvements** +* Support lists comprehension at do-notation ## Release 4.4.0 From 0e9a1adef171fbc813d52ae6dfdfa8c45d837ff8 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Thu, 30 May 2019 14:19:56 +0300 Subject: [PATCH 16/18] Update contribution guideline --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bd89058..a3aa6dc 100644 --- a/README.md +++ b/README.md @@ -42,13 +42,15 @@ The stable library release is available via hex packages, add the library as dep ## How To Contribute -The library is Apache 2.0 licensed and accepts contributions via GitHub pull requests: +The library is [Apache 2.0](LICENSE) licensed and accepts contributions via GitHub pull requests: -* Fork the repository on GitHub -* Read build instructions -* Make a pull request +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request -The build process requires [Erlang/OTP](http://www.erlang.org/downloads) version 19.0 or later and essential build tools. +The development requires [Erlang/OTP](http://www.erlang.org/downloads) version 19.0 or later and essential build tools. **Build** and **run** service in your development console. The following command boots Erlang virtual machine and opens Erlang shell. From a4bb2162f2c58ee86ec6e112d6607329bce294d4 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Thu, 30 May 2019 14:23:30 +0300 Subject: [PATCH 17/18] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a3aa6dc..0effe07 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ The stable library release is available via hex packages, add the library as dep {deps, [{datum}]}. ``` +Please follow the [feature overview](doc/features.md) to start leaning all available features; then continue to library [examples](examples) and to [source code](src). + ## How To Contribute From 79713f74b23268f0c5c517f8727fdc2c830b480a Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Thu, 30 May 2019 14:44:24 +0300 Subject: [PATCH 18/18] Fine tune the generic description --- CHANGELOG.md | 3 ++- doc/generic.md | 26 ++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd41646..be30898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,16 @@ The library uses [semantic versions](http://semver.org) to identify stable relea **Features** * [#68](https://github.com/fogfish/datum/issues/68): Implements generic representations +* Support product lenses for records (`lens:p/1`). ## Release 4.5.x **Features** * [#54](https://github.com/fogfish/datum/issues/54): Implements pattern matching within do-notation -* Support product lenses for records (`lens:p/1`). * Add typecast utility for scalar types. * Support MFA generator for streams +* implements lens isomorphism (`lens:iso/2`, `lens:iso/4`) and product lens (`lens:p/1`). **Improvements** diff --git a/doc/generic.md b/doc/generic.md index 474caee..5e9e89f 100644 --- a/doc/generic.md +++ b/doc/generic.md @@ -2,9 +2,9 @@ > Generic programming is a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters. -- said by [Wikipedia](https://en.wikipedia.org/wiki/Generic_programming). -Erlang permits writing a generic code. It is known to be dynamic, strong typing language, the article [Types (or lack thereof)](https://learnyousomeerlang.com/types-or-lack-thereof) describes it in excellent manner. The in-depth study about type systems have been presented at [Point Of No Local Return: The Continuing Story Of Erlang Type Systems](http://www.erlang-factory.com/static/upload/media/1465548492405302zeeshanlakhanipointofnolocalreturn.pdf). +Erlang permits writing a generic code. It is known to be dynamic, strong typing language, the article [Types (or lack thereof)](https://learnyousomeerlang.com/types-or-lack-thereof) describes it in excellent manner. The in-depth study about type systems have been presented at [Point Of No Local Return: The Continuing Story Of Erlang Type Systems](http://www.erlang-factory.com/static/upload/media/1465548492405302zeeshanlakhanipointofnolocalreturn.pdf). In this section, we would not discuss type systems. Instead we look on other aspect closely related to types. -Type theory and a functional programming also operates with [algebraic data types](https://en.wikipedia.org/wiki/Algebraic_data_type). They are known as a composition of other types. The theory defines two classes of compositions: product types (tuples, records) and co-product types (sum, enumeration or variant types). Product types are strongly expressed by [records](http://erlang.org/doc/reference_manual/records.html) in Erlang; co-products are loosely defined (we skip them at current release). +Type theory and a functional programming operates with [algebraic data types](https://en.wikipedia.org/wiki/Algebraic_data_type). They are known as a composition of other types. The theory defines two classes of compositions: product types (tuples, records) and co-product types (sum, enumeration or variant types). Product types are strongly expressed by [records](http://erlang.org/doc/reference_manual/records.html) in Erlang; co-products are loosely defined (we skip them at current library release). ```erlang -type fullname() :: binary(). @@ -52,21 +52,21 @@ As an example, a typical macro here to cast record type to the map: ). ``` -Unfortunately, usage of records have a couple of disadvantages that make them hardly usable in large projects: +Unfortunately, usage of records have a couple of disadvantages that makes they usage questionable: -* external serialization of domain models often requires knowledge of internals about records: types, attributes names, cardinality, etc. This information is not implicitly available unless you import `.hrl` file with records definition. +* external serialization of domain models often requires knowledge of internals about records: types, attributes names, cardinality, etc. This information is not implicitly available unless you import `.hrl` file with records definition. It will be available for you only at compile time. * record transformations to other record or any external format requires boilerplate code. * generic programming with records requires macros. -* new built-in data type `maps()` obsoletes usage of records for casual use-cases. +* new built-in data type `map()` obsoletes usage of records for casual use-cases. For these reasons, we would like to improve records runtime flexibility through `parse_transform` while keeping its compile-time benefits for domain modeling. -The latest release of library aims on most common tasks in software engineering: representation switching. +The latest release of library aims on most common tasks in software engineering: representation switching or data transformation. ## Switching representations -`datum` provides a `parse_transform` called `generic` that allows us to switch back and forth between a concrete records (ADT) and its generic representa􏰀on without boilerplate. Please not that the generic representation is opaque structure for your code. Please follow examples of generic [here](../examples/src/examples_generic.erl). +`datum` provides a `parse_transform` called `generic` that allows us to switch back and forth between a concrete records (ADT) and its generic representation without boilerplate. Please not that the generic representation is opaque structure for your code. Please follow examples of generic [here](../examples/src/examples_generic.erl).