DEV Community

Cover image for Impeachment in a functional way
Jakob Christensen
Jakob Christensen

Posted on • Originally published at leruplund.dk on

Impeachment in a functional way

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:

  1. Investigation in the House
  2. House vote
  3. 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, aand 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 exitAtparameter 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:

Top comments (6)

Collapse
 
shimmer profile image
Brian Berns • Edited

Kleisli composition! :)

It's great to see another F# developer here. I learned something new from your code when I saw the fish operator defined as an infix function that takes three arguments. I didn't even know that was possible. I guess F# knows that the >=> always goes between the first two arguments? I found it a little easier to understand when I rewrote mentally it as:

let (>=>) a b =
    fun x ->
        match a x with
        | Impeach v -> b v
        | DontImpeach reason -> DontImpeach(reason)
Enter fullscreen mode Exit fullscreen mode

Your version is shorter and cleaner, though, so I certainly don't object.

The other thought I had is a minor suggestion. Functions that match on their last argument are a pretty common pattern in F#, so there's a bit of syntactical sugar you can use if you want to tighten them up. Instead of this:

let tryInSenate exitAt =
    match exitAt with
    | Trial -> DontImpeach (Reason "Senate trial exonerated the President")
    | _ -> Impeach exitAt
Enter fullscreen mode Exit fullscreen mode

You can write:

let tryInSenate = function
    | Trial -> DontImpeach (Reason "Senate trial exonerated the President")
    | exitAt -> Impeach exitAt
Enter fullscreen mode Exit fullscreen mode

Of course, the downside to this approach is that the argument disappears entirely ("point-free" style), which can make the code harder to understand. Just wanted to mention it as an idea.

Lastly, on a purely humorous note, since you're concerned about the bias of using the Result type, you missed a great opportunity to use a Haskell-style Either type instead, since its constructors are Left and Right!

Collapse
 
t4rzsan profile image
Jakob Christensen • Edited

Thank you for your reply. You made my day for suggesting Either 🤣🤣🤣. Now I feel like rewriting the whole thing with Left and Right.

I use Wlaschin's syntax for >=> while Milewski writes it like you do in his section on Kleisli categories (although in Haskell). The two implementations must be equal because of partial application (?). In a way I actually like your way better because it is very clear that the operator returns a function.

I hardly ever use point free-free programming since it makes debugging so much harder. Microsoft has a couple of guidelines on that.

Calling me an F#-developer is a bit of a stretch though and I am still learning. None of my colleagues have seen the light and they all use C#. So I am yet to write a complete project in F# which is such a shame.

Thanks for reading! I look forward to reading your articles on F#!

Collapse
 
shimmer profile image
Brian Berns • Edited

I tend to agree with you (and Microsoft) about point-free style, although there are some cases where it's absolutely essential (e.g. parser combinators). I even wrote a blog post about this here. However, in the function case, the compiler desugars it back to your original code with an argument called _arg1, so it can be easily inspected in a debugger.

I hope you keep going in your F# journey. I was/am in a similar situation, although I've at least convinced my colleagues to allow a few of my F# projects side-by-side with their C# projects. Thanks to .NET, they interop without problems.

Thread Thread
 
t4rzsan profile image
Jakob Christensen

Thanks for the tip on point-free style.

I have been mixing C# and F# projects in the same solution as well and the interop works perfectly as you say but I find the tooling support to be troublesome. If you use "Find all references" or "Go to definition" in Visual Studio I cannot get it to work across different project types. Did you find a solution for that?

Thread Thread
 
shimmer profile image
Brian Berns

I've had the same experience. Tooling support is definitely not perfect, but Visual Studio is still the best IDE out there, I think.

Thread Thread
 
t4rzsan profile image
Jakob Christensen

VS is definitely the best. I have been doing some macOS/iOS stuff lately and XCode is a pain compared to VS.