
Pipelines.

What is a Pipeline?
Many will know the tried a tested GoF pattern to reduce the appearance of nested case statements – The Chain of Responsibility. It works by splitting up the statements into their own class implementations, and creating a chain of them (The output of the first goes in as the input of the second, and so on). At any point, the chain can exit, meaning the rest of the chain is not run; exactly the way a dump of nested case statements would work.
If you are well versed in Haskell, you may know that this type of behaviour can be achieved using Maybe Monads and the >>= operator:
firstFunc input >>= secondfunc >>= ... >>= lastFunction
This is probably something more like what we want to achieve in Erlang, given it’s a functional language. But there are not user defined infix operators in erlang (barring parse transforms, as shown here: https://github.com/pouriya/pipeline).
Jumping in
Well, ignoring the roadblock ahead, we can start by defining a maybe object in erlang:
-record(just, {val :: any()}).
-record(nothing, {}).
-type just(Type) :: {just, Type}.
-type nothing() :: {nothing}.
-type maybe(Type) :: just(Type) | nothing().
Now lets create a chaining operator. It needs to take in a function, and a Maybe value, and either apply the function to the value within the monad (Lifting?) if it’s a just value, or just return the nothing value.
-spec '?>'(fun((any()) -> maybe(any())), maybe(any())) -> maybe(any()).
'?>'(Function, #just{val = Val}) ->
Function(Val);
'?>'(_Function, Maybe = #nothing{}) ->
Maybe.
Ok, so now we have that, we can preform a rubbish chain of responsibility pattern (Given no infixing). First we need the functions that will be used:
do_something(Str) ->
#just{val = Str}.
now_do_thing(Str) ->
#just{val = Str}.
and_then_do_this(Str) ->
#just{val = Str}.
And now we can chain it!
'?>'(and_then_do_this, '?>'(now_do_thing, do_something(Data))).
Wow!
So now to make it not rubbish.
Chain Function
If we take a step back a think about what this is doing, we realise this is a type of fold over a list. Usually, we would use a fold to apply a function on a list of variables. However now, we are using a list of functions and applying them to an input variable. With this piece of knowledge, we can create the following chain function:
-spec chain(list(fun((any()) -> maybe(any()))), any()) -> maybe(any()).
chain([], Input) ->
#just{val = Input};
chain([Func | Funcs], Input) ->
lists:foldl(
fun(IterFunc, Acc) ->
'?>'(IterFunc, Acc)
end,
Func(Input),
Funcs
).
This function takes in a list of functions, and input data. It applies the first function of the input data, then does a foldl over the rest of the functions. At each point, the ‘?>’ operator is used to apply the function to the result of the previous function. If the result is a just, the function is run. If its a nothing, the nothing is returned without the function running.
Usage
ReturnMaybe = chain([
do_something/1,
now_do_thing/1,
and_then_do_this/1,
],
"On This Data"),
And that’s it. The above will run do_something(“On This Data”). That function will return a maybe. If it’s a just, now_do_thing(ReturnVal) will run. If that passes, and_then_do_this(ReturnVal) will then run. A maybe will be returned, which you can check for the result.
An extension to this could involve adding an error message to the nothing record, and logging data on why the chain exited early:
-record(nothing, {error_message :: binary()}).
-type nothing() :: {nothing, binary()}.
And that’s it. Hope that proved useful to someone. If you have any comments, please leave them below!