├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── TODO ├── erlang.mk ├── example.rec ├── include └── lens.hrl ├── rebar.config ├── src ├── Makefile ├── forge.app.src ├── lens.erl └── prism.erl ├── stash ├── eunit_SUITE.erl └── trecord_test.erl └── test └── lens_eqc.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | *.o 4 | *.beam 5 | *.plt 6 | erl_crash.dump 7 | .erlang.mk.packages.v1 8 | *.beam 9 | *.app 10 | logs 11 | current_counterexample.eqc 12 | .eqc-info 13 | bar_gen.erl 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | 3 | sudo: false 4 | script: "make" 5 | 6 | otp_release: 7 | - 18.0 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 Jesper Louis Andersen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = forge 2 | .DEFAULT_GOAL = app 3 | 4 | include erlang.mk 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Forge 2 | 3 | The `forge` package provides ways to *melt* and *recast* Erlang data. That is, it provides a way to help Erlang programmers take data and restructure it so it matches what they need. 4 | 5 | The current implementations for doing this are: 6 | 7 | * `lens` : A library for non-failing functional lenses 8 | 9 | # Lens Tutorial 10 | 11 | Lenses work a bit like the `sofs` library for Erlang: once you know how to use it, you can express thing that were previously hard to do in Erlang. Lenses solve the problem of working with deep nested data structures without picking them apart all the time. Rather, you use lenses to define what you want to change, and then you apply those lenses to change the structure. It gives you the equivalent of a pointed update in imperative languages: 12 | 13 | struct.a.b.c = 3 14 | 15 | but in a pure functional setting. 16 | 17 | Lenses are useful in the following situations: 18 | 19 | * You often manipulate specific data in a deeply nested structure. 20 | * You need to manipulate many sub-parts of a deeply nested structure at the same time. 21 | * You end up writing many functions pattern matching their way to the target element. 22 | 23 | Their use is not on data, where you have no need to go deep into the nesting. A simple pattern match tend to be enough to pick apart the data structure you are working on. 24 | 25 | Here is the internal `lens` record: 26 | 27 | #lens { viewer, setter } 28 | 29 | It contains two functions, the *view* and the *set* function. They work together with an object, normally called the *target* of the lens. Here is the mnemonic idea: the lens provides a optical way to operate on the target. You can *view* the target through the lens, which focuses on a sub-part of the target. And you can *set* that subpart through the lens, which updates that part only, but keeps the rest of the *target* unchanged. 30 | 31 | **Example:** If we have a tuple `{a, 10}`, we can define a view on the 1st element of that tuple: `V = fun(T) -> element(1, T) end`. And we can set that element as well to a new value with a function `S = fun(T, NewVal) -> setelement(1, T, NewVal) end`. Together these form a lens. This lens is also available by calling the function `lens:element(1)`. We can use the lens like follows: 32 | 33 | 1> L = lens:element(1), 34 | 2> lens:v(L, {a, 10}). 35 | a 36 | 37 | The `v/2` function uses the view of the lens. Similarly, we can use the `s/3` function for setting things via the lens: 38 | 39 | 3> lens:s(L, {a, 10}, b). 40 | {b,10} 41 | 42 | ## Lens Rules 43 | 44 | The lens you saw has a peculiar relation between the view and the set function. These are codified in the *lens rules*. All lenses you define must obey the rules, and it is up to you to check that they do. We assume a specific lens with functions `view` and `set` here: 45 | 46 | * [PutGet] for any target T and value A: `A = view(set(T, A))`. 47 | * [GetPut] for any target T: `T = set(T, view(T))`. 48 | * [PutPut] for any target T and values A, A1: `set(T, A1) = set(set(T, A), A1)` 49 | 50 | These rules state that the lens behaves "as it ought to" when operating on data. In particular, it establishes a relation between the view and the set function of the lens. The rules are important because they make sure the lenses we define are well-behaved, even if we start to combine them. 51 | 52 | ## Combining lenses 53 | 54 | The power of lenses are that they can be combined, or composed into larger lenses. This is what makes them effective for updating complex data structures, by picking the structure apart and joining it together again. Like in a camera, we can combine several lenses in order to glean inside more complex structure: 55 | 56 | 4> L2 = lens:element(2). 57 | 5> L3 = lens:compose(L, L2). 58 | 6> lens:v(L3, {{x, y}, {3, 5}}). 59 | y 60 | 61 | The composition says: "run L and then run L2 on the result". So we pick the first element, and then we pick the second element of the result, obtaining `y`. But we can also use the lens to *set* that element: 62 | 63 | 7> lens:s(L3, {{x, y}, {3, 5}}, z). 64 | {{x,z},{3,5}} 65 | 66 | which is the power of the lens-abstraction: it allows us to dig "deep" inside structure and update data deeply. 67 | 68 | ## Lenses on Tuples / Records 69 | 70 | We've already met the tuple lens: `lens:element(K)` for a given `K`. This lens can also work on records, as 71 | 72 | RL = lens:element(#rec.a), 73 | 74 | will define a lens of the record value of `a`. I.e., given a record `R`, we have: 75 | 76 | R#rec.a = lens:v(RL, R) 77 | 78 | ## Lenses on Lists 79 | 80 | On lists, lenses introduces a new way of operating. There is a lens, `lens:nth(K)` for a `K` which operates on the `K`th element of a list. But this lens is often rather slow to use for large lists, so use it with caution. 81 | 82 | But note that there is a *join* over lists with lenses as well. In a join, we combine several lenses into a view of the structure: 83 | 84 | 8> 85 | E1 = lens:element(1), 86 | E2 = lens:element(3), 87 | T = {1,2,3,4}, 88 | JL = lens:join_list([E1, E2]), 89 | lens:v(JL, T). 90 | 91 | [1,3] 92 | 93 | And of course, we can use the join to set elements as well: 94 | 95 | 9> lens:s(JL, T, [a, b]) 96 | {a,2,b,4} 97 | 98 | ## Lenses on Maps 99 | 100 | On maps, you have the lens `lens:key(Key)` where `Key` is the map key to focus on. It works just like every other lens: 101 | 102 | 9> L = lens:key(a), 103 | 10> lens:v(L, #{ a => 3, 10 => 20 }). 104 | 3 105 | 11> lens:s(L, #{ a => 3, 10 => 20 }, xyzzy). 106 | #{10 => 20,a => xyzzy} 107 | 108 | There is also a join variant on hashes: 109 | 110 | > T = {1,2,3,4}, 111 | > E1 = lens:element(1). 112 | > E2 = lens:element(2). 113 | > JL = lens:join_map(#{ a => E1, b => E2}). 114 | > lens:v(JL, T). 115 | #{ a := 1, b := 2} 116 | 117 | Where the set function works in the obvious way: 118 | 119 | > lens:s(JL, T, #{ a => x, b => y}). 120 | {x,y,3,4} 121 | 122 | 123 | # Prism tutorial 124 | 125 | Lenses work on things which are "products" in nature. That is, they work on objects which is a X *and* an Y *and* a Z, …, and so forth. The dual notion is that of a prism, which works on things which are A *or* B *or* … in nature. In other words, they operate over sum-types. A sum which is commonly used in Erlang is the computation with failure: 126 | 127 | {ok, Term} | {error, Reason} 128 | 129 | We can define a prism for this structure. A prism is given by a pair of functions, just like the lens 130 | 131 | #prism { preview, review } 132 | 133 | where the types of the preview and review are "inverted". The function `prism:pre(P, Obj) -> {ok, V} | undefined` defines the preview function of a prism. Given a prism, we can "focus" inside an object to obtain either a value, or undefined. We have the prism `prism:ok()` which focuses on the `ok` part of the above error type. So we have: 134 | 135 | 12> P = prism:ok(). 136 | 13> prism:pre(P, {ok, xyzzy}). 137 | xyzzy 138 | 14> prism:pre(P, {error, not_found}). 139 | undefined 140 | 141 | Likewise, the function `prism:re(P, V) -> Obj`, will hoist our value inside an object by wrapping it into a result. For our *ok-prism* this correspond to wrapping a value into an ok-tuple 142 | 143 | 15> prism:re(P, abracadabra), 144 | {ok, abracadabra} 145 | 146 | ## Prism laws 147 | 148 | Like the lenses, prisms must support certain laws for them to work. In particular, they must support relations between their *preview* and *review* functions. The first one is the following rule: 149 | 150 | preview (review A) = {ok, A} 151 | 152 | it states that there is an identity between wrapping a value `A` and then unwrapping it again. Furthermore, there is a rule in the other direction: 153 | 154 | preview X = {ok, A} ===> (implies) 155 | review A = X 156 | 157 | That is, if we have a prism which can succesfully focus on a value `A`, then reviewing that value reconstructs the original structure *exactly*. That is, there is no other structure in a prism. 158 | 159 | # Traversals 160 | 161 | … 162 | 163 | # QuickCheck 164 | 165 | QuickChecked libraries: 166 | 167 | * `lens` - Almost every function under QuickCheck, but the model still needs some specification to become entirely stable. In particular, I want to generate a lens together with a generator for an underlying object for that lens. This allows one to test the deeply nested lens structure easily by invoking the nested generators as we go along. 168 | 169 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | • Move test code to the test directory 2 | • Move code, we don't need right away into a stash. 3 | -------------------------------------------------------------------------------- /example.rec: -------------------------------------------------------------------------------- 1 | %% This is an example record file for use with the code system 2 | 3 | -record(foo 4 | { a = 10 :: integer() `field_a`, 5 | b = <<"Hello">> :: binary() `field_b` 6 | }). 7 | 8 | %% This record definition maps to the following JSON structure: 9 | %% 10 | %% { 11 | %% "field_a" : 10, 12 | %% "field_b" : "Hello" 13 | %% } 14 | 15 | %% And we will export the functions 16 | %% from_json/1 and to_json/1 for this function in question when compiling from this spec. 17 | -------------------------------------------------------------------------------- /include/lens.hrl: -------------------------------------------------------------------------------- 1 | 2 | %% Since Erlang doesn't support UTF8 atoms yet, we use this Latin1 marker as the 3 | %% "Omega" symbol in Erlang at the moment. 4 | -define(OMEGA, '«·Omega·»'). 5 | 6 | %% To implement a lens, implement the functions g and s given here, subject to the following rules: 7 | %% (1) Assume R is the type of records, and X is the value returned by the lens 8 | %% (2) g should have type R → X 9 | %% (3) s should have type (X, R) → R 10 | %% (4) The lens laws should be respected: 11 | %% (4a) getput : forall R : s(g(R), R) == R 12 | %% (4b) putget : forall R, X : g(s(X, R)) = X 13 | %% And for very-well-behaved lenses 14 | %% (4c) putput : forall R, X, Y, s(Y, s(X, R)) == s(Y, R) 15 | -record(lens, {g, s}). 16 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {deps, []}. 2 | {erl_opts, [debug_info,warn_export_vars,warn_shadow_vars,warn_obsolete_guard]}. 3 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | app: 2 | $(MAKE) -C .. app 3 | -------------------------------------------------------------------------------- /src/forge.app.src: -------------------------------------------------------------------------------- 1 | {application, forge, 2 | [ 3 | {description, "Melt down unstructured data and recast it into beauty"}, 4 | {vsn, "0.9.0"}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib, 9 | parse_trans 10 | ]}, 11 | {modules, []}, 12 | {env, []} 13 | ]}. 14 | -------------------------------------------------------------------------------- /src/lens.erl: -------------------------------------------------------------------------------- 1 | %% -*- coding: utf-8 -*- 2 | %% module lens implements functional lenses for erlang 3 | -module(lens). 4 | 5 | %% Creating lenses 6 | -export([make/2]). 7 | 8 | %% View/Set 9 | -export([v/2, s/3]). 10 | -export([view/1, set/1]). 11 | 12 | %% Transformation 13 | -export([transform/3]). 14 | 15 | %% Composition 16 | -export([compose/1, compose/2]). 17 | 18 | %% Joins 19 | -export([join_list/1, join_map/1]). 20 | 21 | %% Standard Lenses 22 | -export([ 23 | element/1, 24 | nth/1, 25 | key/1, 26 | id/0 27 | ]). 28 | 29 | -record(lens, { 30 | viewer, 31 | setter 32 | }). 33 | 34 | make(Viewer, Setter) -> 35 | #lens { viewer = Viewer, setter = Setter }. 36 | 37 | view(#lens { viewer = V }) -> V. 38 | set(#lens { setter = S }) -> S. 39 | 40 | v(#lens { viewer = V }, T) -> V(T). 41 | s(#lens { setter = S }, T, Val) -> S(T, Val). 42 | 43 | transform(#lens { viewer = V, setter = S}, D, F) -> 44 | S(D, F(V(D))). 45 | 46 | compose(#lens { viewer = V1, setter = S1}, #lens { viewer = V2, setter = S2}) -> 47 | #lens { 48 | viewer = fun(X) -> V2(V1(X)) end, 49 | setter = fun(T, Value) -> 50 | SubT = V1(T), 51 | NSubV = S2(SubT, Value), 52 | S1(T, NSubV) 53 | end 54 | }. 55 | 56 | compose(Args) -> 57 | lists:foldr(fun compose/2, id(), Args). 58 | 59 | %% -- JOINS ------------------------------------ 60 | join_list(Lenses) -> 61 | #lens { 62 | viewer = fun(T) -> [(view(L))(T) || L <- Lenses] end, 63 | setter = fun(T, Vs) -> join_list_set(T, Vs, Lenses) end 64 | }. 65 | 66 | join_list_set(T, [], []) -> T; 67 | join_list_set(T, [V|Vs], [L|Ls]) -> 68 | NewT = (set(L))(T, V), 69 | join_list_set(NewT, Vs, Ls). 70 | 71 | join_map_set(T, _, []) -> T; 72 | join_map_set(T, VMap, [{K, L} | Ls]) -> 73 | V = maps:get(K, VMap), 74 | Res = s(L, T, V), 75 | join_map_set(Res, VMap, Ls). 76 | 77 | join_map(LMap) -> 78 | Base = maps:to_list(LMap), 79 | #lens { 80 | viewer = fun(T) -> 81 | Res = [{K, v(L, T)} || {K, L} <- Base], 82 | maps:from_list(Res) 83 | end, 84 | setter = fun(T, VMap) -> join_map_set(T, VMap, Base) end 85 | }. 86 | %% -- BASE LENSES ---------------------------------- 87 | id() -> 88 | #lens { 89 | viewer = fun(X) -> X end, 90 | setter = fun(_, X) -> X end 91 | }. 92 | 93 | %% -- LENSES ON TUPLES --------------------------- 94 | element(N) -> 95 | #lens { 96 | viewer = fun(Tup) -> element(N, Tup) end, 97 | setter = fun(Tup, Val) -> setelement(N, Tup, Val) end 98 | }. 99 | 100 | %% -- LENSES ON LISTS ----------------------------------- 101 | nth(N) -> 102 | #lens { 103 | viewer = fun(L) -> lists:nth(N, L) end, 104 | setter = fun(L, V) -> 105 | {First, [_|Tail]} = lists:split(N-1, L), 106 | First ++ [V|Tail] 107 | end 108 | }. 109 | 110 | %% -- LENSES ON MAPS ------------------------------------ 111 | key(Key) -> 112 | #lens { 113 | viewer = fun(M) -> maps:get(Key, M) end, 114 | setter = fun(M, V) -> maps:put(Key, V, M) end 115 | }. 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/prism.erl: -------------------------------------------------------------------------------- 1 | -module(prism). 2 | 3 | -export([preview/1, review/1]). 4 | -export([p/2, r/2]). 5 | 6 | -export([ok/0]). 7 | 8 | -record(prism, { 9 | pre, 10 | re 11 | }). 12 | 13 | preview(#prism { pre = P }) -> P. 14 | review(#prism { re = R }) -> R. 15 | 16 | p(#prism { pre = Pre }, Obj) -> Pre(Obj). 17 | r(#prism { re = Re }, Val) -> Re(Val). 18 | 19 | ok() -> 20 | #prism { 21 | pre = fun 22 | ({ok, V}) -> V; 23 | ({error, _}) -> undefined 24 | end, 25 | re = fun(V) -> {ok, V} end 26 | }. 27 | 28 | 29 | -------------------------------------------------------------------------------- /stash/eunit_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2013, Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -module(eunit_SUITE). 16 | 17 | -include_lib("common_test/include/ct.hrl"). 18 | 19 | %% ct. 20 | -export([all/0]). 21 | 22 | %% Tests. 23 | -export([eunit/1]). 24 | 25 | %% ct. 26 | 27 | all() -> 28 | [eunit]. 29 | 30 | eunit(_) -> 31 | ok = eunit:test({application, forge}). 32 | -------------------------------------------------------------------------------- /stash/trecord_test.erl: -------------------------------------------------------------------------------- 1 | -module(trecord_test). 2 | 3 | -compile({parse_transform, trecord}). 4 | 5 | -export([x/0, y/0]). 6 | 7 | -record(bar, 8 | {baz = 3, quux} 9 | ). 10 | 11 | -trecord({foo, 12 | [{a, #{ default => 1, json => "a_tag" }}, 13 | {b, #{ json => "b_tag" }} 14 | ]}). 15 | 16 | x() -> 17 | Foo = #foo{}, 18 | Bar = #bar{}, 19 | io:format("~p~n", [[{foo, Foo}, {bar, Bar}]]). 20 | 21 | y() -> 22 | Foo = #foo{}, 23 | foo_to_jsx(Foo). 24 | -------------------------------------------------------------------------------- /test/lens_eqc.erl: -------------------------------------------------------------------------------- 1 | %%% @doc QuickCheck tests for lenses 2 | %%% @end 3 | -module(lens_eqc). 4 | 5 | -include_lib("eqc/include/eqc.hrl"). 6 | 7 | -compile(export_all). 8 | 9 | keys() -> 10 | elements([a,b,c,d,e,f]). 11 | 12 | %% THE THREE PROPERTIES OF LENSES: 13 | lens_props(Gen, Val, L) -> 14 | View = lens:view(L), 15 | Set = lens:set(L), 16 | ?FORALL({X, A, A1}, {Gen, Val,Val}, 17 | conjunction( 18 | [{putget, equals(A, View(Set(X, A)))}, 19 | {putput, equals(Set(X, A1), Set(Set(X, A), A1))}, 20 | {getput, equals(X, Set(X, View(X)))}, 21 | {transform, 22 | ?LET(F, function1(Val), 23 | equals(F(View(X)), 24 | View(lens:transform(L, X, F))))}])). 25 | 26 | prop_id() -> 27 | ID = lens:id(), 28 | Gen = int(), 29 | Val = int(), 30 | lens_props(Gen, Val, ID). 31 | 32 | tuple(K, Gen) -> 33 | ?LET(V, vector(K, Gen), 34 | list_to_tuple(V)). 35 | 36 | prop_element() -> 37 | ?FORALL(K, choose(1, 5), 38 | ?LET(E, choose(1, K), 39 | begin 40 | Gen = tuple(K, int()), 41 | Val = int(), 42 | lens_props(Gen, Val, lens:element(E)) 43 | end)). 44 | 45 | prop_nth() -> 46 | ?FORALL(K, choose(1,5), 47 | ?LET(E, choose(1, K), 48 | begin 49 | Gen = vector(K, int()), 50 | Val = int(), 51 | lens_props(Gen, Val, lens:nth(E)) 52 | end)). 53 | 54 | g_map(Key) -> 55 | ?LET(M, eqc_gen:map(keys(), int()), 56 | M#{ Key => undefined }). 57 | 58 | prop_map() -> 59 | ?FORALL(Key, keys(), 60 | begin 61 | Gen = g_map(Key), 62 | Val = int(), 63 | lens_props(Gen, Val, lens:key(Key)) 64 | end). 65 | 66 | t() -> 67 | eqc:module({testing_budget, 5}, ?MODULE). 68 | --------------------------------------------------------------------------------