├── .gitignore ├── LICENSE ├── README.md ├── demo ├── data.txt └── fancyflow_demo.erl ├── include └── fancyflow.hrl ├── rebar.config ├── rebar.lock ├── src ├── fancyflow.app.src ├── fancyflow.erl └── fancyflow_trans.erl └── test └── fancyflow_SUITE.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea 17 | rebar3.crashdump 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Fred Hebert . 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | * The names of its contributors may not be used to endorse or promote 16 | products derived from this software without specific prior written 17 | permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FancyFlow 2 | ===== 3 | 4 | FancyFlow is an experimental Erlang library to bring convenience of things like the elixir pipe operator into Erlang, without needing to introduce new syntax (although we do play with existing stuff to avoid semantic confusion). It also allows some more flexibility by allowing the user to choose the placement of the state being weaved through. 5 | 6 | It's a toy, but I'm open to feedback. 7 | 8 | Usage 9 | ----- 10 | 11 | Add the `fancyflow_trans` to your modules or applications, and use any of the control flow functions: 12 | 13 | ```erlang 14 | [pipe](InitialState, Exp1, Exp2, ..., ExpN) 15 | [maybe](InitialState, Exp1, Exp2, ..., ExpN) 16 | [parallel](Exp1, Exp2, ..., ExpN) 17 | ``` 18 | 19 | Where each expression can be a valid Erlang expression, with the state substituted by the `_` variable. 20 | 21 | For example: 22 | 23 | ```erlang 24 | -module(fancyflow_demo). 25 | -export([sans_pipe/0, pipe/0, 26 | sans_maybe/0, maybe/0]). 27 | 28 | -compile({parse_transform, fancyflow_trans}). 29 | 30 | -spec sans_pipe() -> string(). 31 | sans_pipe() -> 32 | String = "a b c d e f", 33 | string:join( 34 | lists:map(fun string:to_upper/1, string:tokens(String, " ")), 35 | "," 36 | ). 37 | 38 | -spec pipe() -> string(). 39 | pipe() -> 40 | [pipe]("a b c d e f", 41 | string:tokens(_, " "), 42 | lists:map(fun string:to_upper/1, _), 43 | string:join(_, ",")). 44 | 45 | -spec sans_maybe() -> {ok, non_neg_integer()} | {error, term()}. 46 | sans_maybe() -> 47 | case file:get_cwd() of 48 | {ok, Dir} -> 49 | case file:read_file(filename:join([Dir, "demo", "data.txt"])) of 50 | {ok, Bin} -> 51 | {ok, {byte_size(Bin), Bin}}; 52 | {error, Reason} -> 53 | {error, Reason} 54 | end; 55 | {error, Reason} -> 56 | {error, Reason} 57 | end. 58 | 59 | -spec maybe() -> {ok, non_neg_integer()} | {error, term()}. 60 | maybe() -> 61 | [maybe](undefined, 62 | file:get_cwd(), 63 | file:read_file(filename:join([_, "demo", "data.txt"])), 64 | {ok, {byte_size(_), _}}). 65 | 66 | -spec sans_parallel() -> [term() | {badrpc, term()}]. 67 | sans_parallel() -> 68 | R1 = rpc:async_call(node(), lists, seq, [1,10]), 69 | R2 = rpc:async_call(node(), filelib, wildcard, ["*"]), 70 | R3 = rpc:async_call(node(), erlang, apply, 71 | [fun() -> timer:sleep(10), slept end, []]), 72 | R4 = rpc:async_call(node(), ets, all, []), 73 | [rpc:yield(R1), rpc:yield(R2), rpc:yield(R3), rpc:yield(R4)]. 74 | 75 | -spec parallel() -> [{ok, term()} | {error, term()}]. 76 | parallel() -> 77 | [parallel](lists:seq(1,10), 78 | filelib:wildcard("*"), 79 | begin timer:sleep(10), slept end, 80 | ets:all()). 81 | 82 | ``` 83 | 84 | The `pipe()` function reworks the `sans_pipe()` function to be equivalent. The `maybe()` one gives the same result as the `sans_maybe()` one, although it ignores the initial state altogether by using `undefined`. The `sans_parallel()` one shows traditional rpc-based handling of parallel operations whereas `parallel()` shows a simpler syntax, without need of work-arounds for unexported calls. 85 | 86 | The expressions must be literal, and may be nested although this isn't always super clear: 87 | 88 | 89 | ```erlang 90 | -spec nested() -> [{ok, _} | {error, _}]. 91 | nested() -> 92 | [parallel]( 93 | %% first operation reads ./demo/data.txt 94 | [maybe](undefined, 95 | file:get_cwd(), 96 | file:read_file(filename:join([_, "demo", "data.txt"]))), 97 | %% second parallel op makes a filename and reads its size if any 98 | [pipe]("a b c d e f", 99 | string:tokens(_, " "), 100 | lists:map(fun string:to_upper/1, _), 101 | string:join(_, ","), 102 | %% Maybe the file doesn't exist 103 | [maybe](_, % the string from [pipe] is a filename here 104 | file:read_file(_), 105 | {ok, {byte_size(_), _}}) 106 | ) 107 | ). 108 | ``` 109 | 110 | With a result set possibly looking like: 111 | 112 | ``` 113 | 1> fancyflow_demo:nested(). 114 | [{ok,{ok,<<"124567890\n">>}}, 115 | {ok,{error,enoent}}] 116 | ``` 117 | Showing what happens if the first file exists (contains `1234567890\n`) and the second one does not (filename `A,B,C,D,E,F`), with both reads happening in parallel. 118 | 119 | It might be a good idea not to nest the expressions too much. 120 | 121 | How it works 122 | ------------ 123 | 124 | Each form of 125 | 126 | ```erlang 127 | [pipe](InitialState, Exp1, Exp2, ..., ExpN) 128 | [maybe](InitialState, Exp1, Exp2, ..., ExpN) 129 | [parallel](Exp1, Exp2, ..., ExpN) 130 | ``` 131 | 132 | is translated to: 133 | 134 | ```erlang 135 | fancyflow:pipe(InitialState, [fun(Var) -> Exp1 end, 136 | fun(Var) -> Exp2 end, 137 | ..., 138 | fun(Var) -> ExpN end]) 139 | fancyflow:maybe(InitialState, [fun(Var) -> Exp1 end, 140 | fun(Var) -> Exp2 end, 141 | ..., 142 | fun(Var) -> ExpN end]) 143 | fancyflow:parallel([fun() -> Exp1 end, 144 | fun() -> Exp2 end, 145 | ..., 146 | fun() -> ExpN end]) 147 | ``` 148 | 149 | Which internally, runs a fold over the functions based on the state. The variable used as a function head is generated dynamically (looks like `_#Ref<0.0.3.1555>`) such that they never conflict with the surrounding scope, and never complain if they are not used. 150 | 151 | the `[Function](...)` syntax has been chosen to not look like a regular function call and so that nobody mistakes its unorthodox (and otherwise illegal) usage of free variables with normal Erlang code. 152 | -------------------------------------------------------------------------------- /demo/data.txt: -------------------------------------------------------------------------------- 1 | 124567890 2 | -------------------------------------------------------------------------------- /demo/fancyflow_demo.erl: -------------------------------------------------------------------------------- 1 | -module(fancyflow_demo). 2 | -export([sans_pipe/0, pipe/0, 3 | sans_maybe/0, maybe/0, 4 | sans_parallel/0, parallel/0, 5 | nested/0]). 6 | 7 | -compile({parse_transform, fancyflow_trans}). 8 | 9 | -spec sans_pipe() -> string(). 10 | sans_pipe() -> 11 | String = "a b c d e f", 12 | string:join( 13 | lists:map(fun string:to_upper/1, string:tokens(String, " ")), 14 | "," 15 | ). 16 | 17 | -spec pipe() -> string(). 18 | pipe() -> 19 | [pipe]("a b c d e f", 20 | string:tokens(_, " "), 21 | lists:map(fun string:to_upper/1, _), 22 | string:join(_, ",")). 23 | 24 | -spec sans_maybe() -> {ok, non_neg_integer()} | {error, term()}. 25 | sans_maybe() -> 26 | case file:get_cwd() of 27 | {ok, Dir} -> 28 | case file:read_file(filename:join([Dir, "demo", "data.txt"])) of 29 | {ok, Bin} -> 30 | {ok, {byte_size(Bin), Bin}}; 31 | {error, Reason} -> 32 | {error, Reason} 33 | end; 34 | {error, Reason} -> 35 | {error, Reason} 36 | end. 37 | 38 | -spec maybe() -> {ok, non_neg_integer()} | {error, term()}. 39 | maybe() -> 40 | [maybe](undefined, 41 | file:get_cwd(), 42 | file:read_file(filename:join([_, "demo", "data.txt"])), 43 | {ok, {byte_size(_), _}}). 44 | 45 | -spec sans_parallel() -> [term() | {badrpc, term()}]. 46 | sans_parallel() -> 47 | R1 = rpc:async_call(node(), lists, seq, [1,10]), 48 | R2 = rpc:async_call(node(), filelib, wildcard, ["*"]), 49 | R3 = rpc:async_call(node(), erlang, apply, 50 | [fun() -> timer:sleep(10), slept end, []]), 51 | R4 = rpc:async_call(node(), ets, all, []), 52 | [rpc:yield(R1), rpc:yield(R2), rpc:yield(R3), rpc:yield(R4)]. 53 | 54 | -spec parallel() -> [{ok, term()} | {error, term()}]. 55 | parallel() -> 56 | [parallel](lists:seq(1,10), 57 | filelib:wildcard("*"), 58 | begin timer:sleep(10), slept end, 59 | ets:all()). 60 | 61 | -spec nested() -> [{ok, _} | {error, _}]. 62 | nested() -> 63 | [parallel]( 64 | %% first operation reads ./demo/data.txt 65 | [maybe](undefined, 66 | file:get_cwd(), 67 | file:read_file(filename:join([_, "demo", "data.txt"]))), 68 | %% second parallel op makes a filename and reads its size if any 69 | [pipe]("a b c d e f", 70 | string:tokens(_, " "), 71 | lists:map(fun string:to_upper/1, _), 72 | string:join(_, ","), 73 | %% Maybe the file doesn't exist 74 | [maybe](_, % the string from [pipe] is a filename here 75 | file:read_file(_), 76 | {ok, {byte_size(_), _}}) 77 | ) 78 | ). 79 | -------------------------------------------------------------------------------- /include/fancyflow.hrl: -------------------------------------------------------------------------------- 1 | -compile({parse_transform, fancyflow_trans}). 2 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {profiles, [ 2 | {demo, [ 3 | {src_dirs, ["src", "demo"]} 4 | ]} 5 | ]}. 6 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /src/fancyflow.app.src: -------------------------------------------------------------------------------- 1 | {application, fancyflow, 2 | [{description, "Experimental library to bring pipe and maybe operator equivalents in Erlang"}, 3 | {vsn, "0.1.0"}, 4 | {registered, []}, 5 | {applications, 6 | [kernel, 7 | stdlib 8 | %,syntax_tools % <- only required at compile time 9 | ]}, 10 | {env,[]}, 11 | {modules, []}, 12 | 13 | {maintainers, ["Fred Hebert"]}, 14 | {licenses, ["BSD"]}, 15 | {links, []} 16 | ]}. 17 | -------------------------------------------------------------------------------- /src/fancyflow.erl: -------------------------------------------------------------------------------- 1 | -module(fancyflow). 2 | 3 | %% API exports 4 | -export([pipe/2, maybe/2, parallel/1]). 5 | 6 | %%==================================================================== 7 | %% API functions 8 | %%==================================================================== 9 | -spec pipe(State, [fun((State) -> State)]) -> State when State :: any(). 10 | pipe(Init, Funs) -> 11 | lists:foldl(fun(F, State) -> F(State) end, Init, Funs). 12 | 13 | -spec maybe(State, [fun((State) -> Return)]) -> Return when 14 | State :: any(), 15 | Return :: {ok, State} | {error, State}. 16 | maybe(Init, Funs) -> 17 | SwitchFun = fun(F, State) -> 18 | case F(State) of 19 | {ok, NewState} -> NewState; 20 | {error, Reason} -> throw({'$return', Reason}) 21 | end 22 | end, 23 | try 24 | {ok, lists:foldl(SwitchFun, Init, Funs)} 25 | catch 26 | {'$return', Term} -> {error, Term} 27 | end. 28 | 29 | -spec parallel([fun(() -> _)]) -> [{ok, _} | {error, _}]. 30 | parallel(Funs) -> 31 | Ref = make_ref(), 32 | ReplyTo = self(), 33 | Pids = [spawn(futurize(F, Ref, ReplyTo)) || F <- Funs], 34 | [gather(Ref, Pid) || Pid <- Pids]. 35 | 36 | 37 | %%==================================================================== 38 | %% Private 39 | %%==================================================================== 40 | -spec futurize(fun(() -> _), reference(), pid()) -> fun(() -> _). 41 | futurize(F, Ref, ReplyTo) -> 42 | fun() -> ReplyTo ! {self(), Ref, 43 | try 44 | {ok, F()} 45 | catch 46 | throw:Val -> {ok, Val}; 47 | error:Reason -> {error, {Reason, erlang:get_stacktrace()}}; 48 | exit:Reason -> {error, Reason} 49 | end} 50 | end. 51 | 52 | -spec gather(reference(), pid()) -> {ok, term()} | {error, term()}. 53 | gather(Ref, Pid) -> 54 | receive 55 | {Pid, Ref, Res} -> Res 56 | end. 57 | -------------------------------------------------------------------------------- /src/fancyflow_trans.erl: -------------------------------------------------------------------------------- 1 | -module(fancyflow_trans). 2 | -export([parse_transform/2]). 3 | -include_lib("syntax_tools/include/merl.hrl"). 4 | 5 | parse_transform(ASTs, _Options) -> 6 | try 7 | [erl_syntax_lib:map(fun(T) -> 8 | transform(erl_syntax:revert(T)) 9 | end, AST) || AST <- ASTs] 10 | catch 11 | _E:_R -> 12 | ASTs 13 | end. 14 | 15 | transform({call, Line, 16 | {cons, CLine, {atom, CLine, pipe}, {nil, CLine}}, 17 | [Init | Funs]}) -> 18 | {call, Line, 19 | {remote, Line, {atom, CLine, fancyflow}, {atom, CLine, pipe}}, 20 | [Init, rework_funs(Funs, CLine, create_var())]}; 21 | transform({call, Line, 22 | {cons, CLine, {atom, CLine, maybe}, {nil, CLine}}, 23 | [Init | Funs]}) -> 24 | {call, Line, 25 | {remote, Line, {atom, CLine, fancyflow}, {atom, CLine, maybe}}, 26 | [Init, rework_funs(Funs, CLine, create_var())]}; 27 | transform({call, Line, 28 | {cons, CLine, {atom, CLine, parallel}, {nil, CLine}}, 29 | Funs}) -> 30 | {call, Line, 31 | {remote, Line, {atom, CLine, fancyflow}, {atom, CLine, parallel}}, 32 | [rework_funs(Funs, CLine)]}; 33 | transform(Term) -> 34 | Term. 35 | 36 | create_var() -> 37 | binary_to_atom(iolist_to_binary(io_lib:format("_~p", [make_ref()])), utf8). 38 | 39 | rework_funs([], Line) -> 40 | {nil, Line}; 41 | rework_funs([F|Funs], Line) -> 42 | {cons, Line, 43 | {'fun', Line, {clauses, 44 | [{clause, Line, [], [], [erl_syntax:revert(F)]}] 45 | }}, 46 | rework_funs(Funs, Line)}. 47 | 48 | rework_funs([], Line, _) -> 49 | {nil, Line}; 50 | rework_funs([F|Funs], Line, VarName) -> 51 | {cons, Line, 52 | {'fun', Line, {clauses, 53 | [{clause, Line, 54 | [{var,Line,VarName}], 55 | [], 56 | [erl_syntax:revert(erl_syntax_lib:map( 57 | fun(V) -> replace_var(V, VarName) end, 58 | F 59 | ))]}] 60 | }}, 61 | rework_funs(Funs, Line, VarName)}. 62 | 63 | replace_var({var, Line, '_'}, VarName) -> 64 | {var, Line, VarName}; 65 | replace_var(Exp, _) -> 66 | Exp. 67 | -------------------------------------------------------------------------------- /test/fancyflow_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(fancyflow_SUITE). 2 | -include_lib("common_test/include/ct.hrl"). 3 | -include_lib("eunit/include/eunit.hrl"). 4 | -compile({parse_transform, fancyflow_trans}). 5 | -compile(export_all). 6 | 7 | all() -> 8 | [pipe, maybe, parallel, 9 | pipe_trans, maybe_trans, parallel_trans, 10 | mixed_trans]. 11 | 12 | pipe(_) -> 13 | ?assertEqual(3, 14 | fancyflow:pipe(0, [ 15 | fun(N) -> N + 5 end, 16 | fun(N) -> N - 2 end 17 | ])). 18 | 19 | maybe(_) -> 20 | ?assertEqual({ok, 3}, 21 | fancyflow:maybe(0, [ 22 | fun(N) -> {ok, N+1} end, 23 | fun(N) -> {ok, N+1} end, 24 | fun(N) -> {ok, N+1} end 25 | ])), 26 | ?assertEqual({error, third_clause}, 27 | fancyflow:maybe(0, [ 28 | fun(N) -> {ok, N+0} end, 29 | fun(N) -> {ok, N+0} end, 30 | fun(_) -> {error, third_clause} end, 31 | fun(N) -> {ok, N+0} end 32 | ])). 33 | 34 | parallel(_) -> 35 | ?assertMatch([{ok, 1}, 36 | {ok, 2}, 37 | {error, {badarith, _}}, 38 | {ok, 4}], 39 | fancyflow:parallel([ 40 | fun() -> timer:sleep(150), 1 end, 41 | fun() -> 1+1 end, 42 | fun() -> 3/(2-2) end, 43 | fun() -> erlang:'+'(2,2) end 44 | ])). 45 | 46 | pipe_trans(_) -> 47 | _ = fun(X) -> id(X) end, 48 | ?assertEqual(3, 49 | [pipe](0, 50 | _ + 5, 51 | id(_), 52 | _ - 2 53 | )). 54 | 55 | maybe_trans(_) -> 56 | ?assertEqual({ok, 3}, 57 | [maybe](0, 58 | {ok, _+1}, 59 | ok_id(_), 60 | {ok, _+1}, 61 | ok_id(_), 62 | {ok, _+1} 63 | )), 64 | ?assertEqual({error, third_clause}, 65 | [maybe](0, 66 | {ok, _+0}, 67 | ok_id(_), 68 | {error, third_clause}, 69 | {ok, _+0} 70 | )). 71 | 72 | id(X) -> X. 73 | ok_id(X) -> {ok,X}. 74 | 75 | parallel_trans(_) -> 76 | ?assertMatch([{ok, 1}, 77 | {ok, 2}, 78 | {error, {badarith, _}}, 79 | {ok, 4}], 80 | [parallel]( 81 | begin timer:sleep(150), 1 end, 82 | 1+1, 83 | 3/(2-2), 84 | erlang:'+'(2,2) 85 | )). 86 | 87 | mixed_trans(_) -> 88 | ?assertMatch({error, {badarith, _}}, 89 | [maybe](0, 90 | hd([parallel](_+1, _+2)), 91 | hd(tl([parallel](_+1, _+2/(1-1)))), 92 | hd([parallel](_+1, _+2)) 93 | )), 94 | ?assertMatch(10, 95 | [pipe](0, 96 | _+1, % 1 97 | _+2, % 3 98 | [maybe](_, % <- 3 99 | {ok, _+3}, % 6 100 | {error, _+1} % 7 101 | ), % 7 102 | element(2,_)+3 % 10 103 | )). 104 | 105 | --------------------------------------------------------------------------------