Functional Programming (FP) did not really click with me until I saw how it utilizes composition of functions to model pipelines of tasks. Lots of sources on the internet mention immutability and algebraic types as great advantages of FP but it was composition that won me over.
In particular, composition is perfect for describing workflows from the real world.
The impeachment process
I came across such a real world workflow in this article on Axios. The process for impeachment and removal from office goes through a number of steps where each step has two possible outcomes: Either the process terminates or it continues.
The steps are:
- Investigation in the House
- House vote
- Senate trial
Our goal is to model the steps in a way that mimics the terminations and continuations of the process. We also want each step to report the reason for a termination.
The non-functional way
In a non-functional language such as C# there are a number of ways you could model the process. One way would be to let each step throw an exception in case the process should be terminated. In that case the overall code would look something like this:
public void RunImpeachmentProcess()
{
try
{
InvestigateInHouse();
VoteInHouse();
TryInSenate();
}
catch (Exception ex)
{
Log($"The impeachment process stopped: {ex.Message}.");
throw;
}
}
It works but it is not very elegant. Exceptions are for exceptional cases and stopping an impeachment process is not exceptional and this way is not very explicit.
Another way would be to let each function return false or true indicating termination or not.
public bool TryRunImpeachmentProcess(out string reason)
{
if (TryInvestigateInHouse(out reason))
{
if (TryVoteInHouse(out reason))
{
return TryTrialInSenate(out reason);
}
}
return false;
}
This also works and it does not look too bad. However, if the number of steps increase above three it will end up as a mess that is hard to follow. There are other variations, for example you could mix in return statements to avoid the triangle of nested if-statement. You may also have to pass information from one step to the next as a parameter which only makes it harder and messier.
The functional way
Enter composition. With composition your code could look something like this (I will be using F#):
let runImpeachmentProcess =
investigateInHouse
>=> voteInHouse
>=> tryInSenate
This is very explicit. The workflow is self-documenting and you can easily see the steps involved.
Let’s see what the implementation looks like. We start with a type that holds the result for a step, whether the process continues or terminates:
type Reason = Reason of string
type ImpeachmentResult<'T> =
| Impeach of ImpeachValue:'T
| DontImpeach of Reason
We could have used the built-in Result
type that can be either Ok
or Error
. Depending on your political standpoint, using Error
for ending the impeachment process may not be the correct term so I created my own ImpeachmentResult
type.
We now add the >=>
(fish-)operator for composing two functions that each return an ImpeachmentResult
:
let (>=>) a b x =
match a x with
| Impeach v -> b v
| DontImpeach reason -> DontImpeach(reason)
The operator takes two functions, a
and b
with the following signatures:
a:'a -> ImpeachmentResult<'b>
b:'b -> ImpeachmentResult<'c>
Hence, the >=>
unwraps the impeachment result of function a
and feeds it into function b
. The parameter x
can be of any type and it does not have to be the same type for each step. I.e. each step may return different results. The only requirement is that the next function takes the same type as parameter.
Let’s add dummy implementations for the three steps (I’ll leave the actual implementation as an exercise for the reader). I have added a exitAt
parameter of type ExitAtStep
to each function to control which step the process should exit at. In the real world you won’t need such a parameter. However, it does show that you can pass parameters from on step to the next, all handled by the >=>
operator:
type ExitAtStep =
| None
| Investigation
| Vote
| Trial
let investigateInHouse exitAt =
match exitAt with
| Investigation -> DontImpeach (Reason "Investigation did not find enough evidence")
| _ -> Impeach exitAt
let voteInHouse exitAt =
match exitAt with
| Vote -> DontImpeach (Reason "Less than two-thirds voted for impeachment in House")
| _ -> Impeach exitAt
let tryInSenate exitAt =
match exitAt with
| Trial -> DontImpeach (Reason "Senate trial exonerated the President")
| _ -> Impeach exitAt
Finally, we get to the workflow function that is a composition of the three steps above:
let runImpeachmentProcess =
investigateInHouse
>=> voteInHouse
>=> tryInSenate
Note that for this example runImpeachmentProcess
is a function of type:
runImpeachmentProcess:ExitAtStep -> ImpeachmentResult<ExitAtStep>
Let’s run it:
let run exitAt =
let result = runImpeachmentProcess exitAt
match result with
| Impeach _ -> printfn "Remove from office."
| DontImpeach (Reason reason) -> printfn "No impeachment for the following reason: %s." reason
If all three steps return Impeach
, it will print out “Remove from office”. If at least one of the three steps return DontImpeach
, the code will print out the reason for the exited process with the reason for the first function that returns DontImpeach
. Subsequent steps will not be called if one returns DontImpeach
. Let’s have a look at the output:
run None
// Remove from office.
run Investigation
// No impeachment for the following reason: Investigation did not find enough evidence.
run Vote
// No impeachment for the following reason: Less than two-thirds voted for impeachment in House.
run Trial
// No impeachment for the following reason: Senate trial exonerated the President.
Closing
Composition gives us a very strong tool for creating explicit workflows for our processes. We are able to create a function composed of other functions that each represent a step in the workflow. The code for the composed function is in itself a documentation of what it does because it very clearly shows the steps:
let runImpeachmentProcess =
investigateInHouse
>=> voteInHouse
>=> tryInSenate
I am in no way the inventor of this method. I just applied it to a world (in)famous ongoing process. Please have a look at the following sources: