Published on

Easy becomes complex

Authors

Problems strike back!

Yet again, the echo of the past got me.

Another bug, this time introduced in codebase written in my beloved F#.

You might think, dear Reader, how bad programmer I am as I am producing so many bugs.

This might be true, not denying that.

Let's see what can we learn this time.

We need more options!

This "subtle" thing slipped through three, maybe four years ago.

It might not have been that hard to fix, but truly it was difficult to find.

Imagine that we are integrate with an API (we don't own it) that is responsible for managing tasks that human beings can pick and work on them.

There's a concept of a "reason" - why this task was created.

Someone decided to model those two concepts in a way that a task might have multiple reasons (why not?).

We could manifest this little problem area as follows:

type Task = {
    Reason: string seq
}

So far so good. We are clearly doing DDD right, aren't we?

Turns out that even though there's a collection of reasons, we were told that in 99.999999% there's only one reason that is attached to a task.

Within our system's boundary, when we consume this information, we must use this reason for presentation purposes so that people can take some actions/decisions based on it.

Ok, so let's play a roulette and try getting the first Reason:

let reason: string option = receivedTasks |> Seq.tryHead 

Boom, that was easy.

Yeah, let's put even more DDD into it!

type Reason = Reason of taskReason: string

type Task = {
    Reason: Reason seq
}

Then our parsing logic becomes:

let reason: Reason option =
    receivedTasks
    |> Seq.tryHead
    |> Option.map Reason

No primitive obsession, right? We are expressing Language of the problem.

No options?

As we got our lovely task with the reason, somewhere down the processing pipeline (in another file), an assumption has been encoded.

The assumption that each task almost always has a reason.

So there was a usage of task's Reason:

let consume (reason: Reason option) =
    //...
    let (Reason reason') = reason.Value
    consumeReasonAsText reason'
    //...

I hope you see the potential issue there.

Never trust the incoming data.

After 3-4 years in production, a task came in without any reason (pun intended).

It turned out that there was a data consistency problem, outside of the our system's boundary.

The bug got fixed, as always fixing it is easier when you know what the problem is.

Can we do a little thought experiment and check what we could do?

Option? I see a result

Yeah, Option might not be the perfect choice there.

No information gets carried, regarding the reason of failure (pun intended).

So maybe then we should use Result type and put meaningful information in the error path?

We would get the following:

let reason: Result<Reason, string> =
    match receivedTasks |> Seq.tryHead with
    | Some reason -> Ok reason
    | None -> Error "missing reason for a task"

Better, at least we see the possible intention.

Still, I am not convinced with this result (pun intened). We expressed the knowledge about the problem as a string.

So even though we are in F#, we did stringly-typed programming.

Shame on me, shame on me.

We know that there is a single scenario that might be considered as almost impossible to be reached - missing reason.

Why not modelling it explicitly?

Then our code becomes:

type GettingReasonError = MissingReason // when more problems are found, add more cases

let reason: Result<Reason, GettingReasonError> =
    match receivedTasks |> Seq.tryHead with
    | Some reason -> Ok reason
    | None -> Error MissingReason

What a blast.

Domain modeling made truly functional.

It's all about the language

As I like using Result type for capturing the intent and modeling failure effects, it does not read well.

let reason: Result<Reason, GettingReasonError> = //...

How would you read it aloud, dear Reader?

Something like: "a reason is either ok of Reason or error of GettingReasonError"?

I assume not.

I might be hugely biased but I feel that language, how do we communicate, is enormously important.

Code is also communicating with us, so it needs to be very clear with its intentions. (you might be interested in Conversation-Driven Design)

That's why I believe Either would be a better choice (it is not available by default in F#):

let reason: Either<Reason, GettingReasonError> = //...

It somehow feels much natural when saying it aloud: "a reason is either a Reason or a GettingReasonError".

Of course, one would say I am cheating because in the previous example I explicitly wrote "ok of Reason" and now I didn't write "left of Reason or right of GettingReasonError".

Well, maybe.

But let's get back to our lovely tale!

What is the case?

We have used built-in type to express the intention of missingness.

Built-in types are great because they are easy to use.

You can just let those types flow through your fingers and boom - it's all there.

Almost effortless work, especially in such a language like F#.

It looks easy, but Simple isn't easy.

One of the conclusions, coming from "Simple made easy" presentation I mentioned in previous blog post was:

Conclusion 🔍

Simplicity often mean making more things, not fewer

Even though it is really easy to utilize built-in types, it might not be simple.

And there are two assumptions, invisible for ourselves when we have such way of thinking: it is easy now, for me.

If we Slow down a bit and re-consider our model, eventually Rethinking "missingness".

In F# we are blessed with discriminated unions and our domain model could be free from any effects (like Option, Result or Async).

One could say: "Damian, but option serves exactly that purpose - it models missingness".

While this might be true, it does not tell us why things are not there.

And Result type is typically associated with "failures".

I would like my model to tell this story (or tale).

What would you say about such model, dear Reader?

type SingleReason = SingleReason of reason: string
type Reason = Expected of SingleReason | MissingReason

When more cases will appear, we would be able to work with this model and let it express the language we would typically use.

Of course, it's just a toy problem and we could argue between using Result and this custom type.

Nevertheless, there's a point to be made.

Don't fool others - including future yourself

Even though we worked with awesome language that F# is - it's still our responsibility to provide a solution to the given problem.

Couple of years back, I did something that was easy.

I don't recall the context - maybe I was rushing as it was super critical to be solved in a limited amount of time.

Maybe my son came to this beautiful world and I was sleeping -2 hours during the night.

Can't really say.

What comes to my mind right now is that quality needs time.

Time for thinking, capturing the right concepts and terms, eventually manifesting them and embedding into the code as our model.

And this is actually one of the biggest impacts of DDD-like thinking - each time we solve a problem, we build a model.

A model of our understanding.

Understanding of the problem - all the concepts, rules between them, their interactions and eventually - relationships.

Our understanding might change, so the model should too.

In F#, expressing your intentions is really, really easy - sometimes it's not simple, as we need to build up the understanding, but the language is so powerful and enabling.

Such exercise that we had throughout this little tale, dear Reader, would not have been that easy in, e.g. C#

Not because C# does not provide means and ways of achieving similar level of expressivity, but because fellow team members might not be used to such a way of thinking.

One might call it "proper modelling", whereas another person might label it as "overengineering".

Everything boils down to "it depends".

Now, for me, an author of this code, it might be easy to use primitive types and not focus on capturing the right language, expressing the concepts living in the domain, via types.

In the future, for future myself or you, it might be crucial to comprehend the code in the effortless way.

Easy becomes complex

Readability is one of the highest qualities of what we are doing.

When I read the code I wrote one week ago, I might consider that I don't know what's there - how quickly can I catch up?

Many times, many people, in many places, stated that programmers more often read code rather than write it.

Easy will become complex, eventually, but now, for me, it might look as the best way of solving this problem.

Don't get me wrong, I am not agitating and advocating building a huge hierarchy of classes or trying to predict the future and adding functionality "just in case".

Reveal your intentions through the code - be expressive, play with your model.

Don't be afraid of using such tools like OneOf, ts-pattern, CSharpFunctionalExtensions or similar (based on your programming language), in order to express your intentions.

Even if they might increase The cost of modeling, it might be just an assumption.

We should ask ourselves - what will I get when I organize the model/the code in such a way? (what do I mean by "organize"? You might be interested in The ambiguity of software architecture).

Next time, dear Reader, Slow down, think about the problem you are trying to solve, and model things accordingly - remember about The value of Value Objects!

Future yourself will thank you (and maybe me?).