Strive for totality in Ruby

Do you remember part 1 and part 2 of my articles about functional programming in Ruby? Here comes yet another blog post related to this topic.

Totality Principle is a concept that for every input for a function there’s a valid output.

This is what functional programmers strive a lot.

The concept

Sometimes we have a use case to handle some kind of input and produce a specific output based on it. Actually, this happens more than often and can be realized by either just a simple method or even a complex service object. So we receive incoming arguments and return a particular result.

In statically typed languages, we would declare a concrete contract which defines what type of parameters we accept and what kind of output we produce.

our_method :: int, bool -> string

We know our_method accepts two arguments with a type of an integer and a boolean, and returns a string result.

But can we ensure a user we are telling the truth? Are we confident we ALWAYS accept every value from a given domain and return a value from a correct codomain as well?

The problem

Let’s say we have the following requirement:

For a given integer return a result of dividing 8 by that number.

Simple enough, right? We can start with a very naive implementation:

We install the auxiliary contracts gem to provide a function signature, and we start implementing our cases. Suddenly, it turns out for a 0 (zero) value there is no output because no number is dividable by 0!

We can handle this in a very convenient way:

But wait a second. Is our function signature still valid? Does it return a number for every number? Not anymore! Mind you we fail for zero which means we no longer handle every input from the declared domain.

The solution

Undetermined value for some argument from our domain can be fortunately handled. How to solve it correctly then? Well, there are some solutions.

Constraint an input

The first alternative is to define a new type which excludes zero from our set. If 0 is missing, we don’t have to support it!

0 no longer have to be handled, and the signature doesn’t lie to you anymore!

Extend an output

The other way to do it is to extend an output. We can use monad-like approach to say we either know or don’t know what the output is.

So in cases when we do know the result we change it into Some and for zero we return None.

Give me any int and I may return you a result.

Now, we have a Maybe return type. The type signature is telling the truth. Keep in mind that in this case 0 is a valid input!

Hollywood principle

a.k.a. “don’t call us, we’ll call you.”

Let’s get back error-driven approach once again:

Who put in charge this method to throw an exception? I see for a given number it returns an other number, so I want to be in charge in case of any errors. I want this signature to tell the truth.

The way to make me in charge is to pass in functions (callbacks) which will be invoked in each case. In this way, I have a complete control over what is going on. If I do this, it will never throw any exception.

We modified our contract a little bit. We accept a number as always, but additionally we take two functions. The former should receive a number (the actual result) and can return basically whatever while the latter may be a regular proc. We defied two behaviours to handle corresponding results from our function with a quite powerful technique.

Subscribe to get the latest content immediately

https://tinyletter.com/KamilLelonek

Summary

Types system may be tricky. There are of course fewer problems when we are using statically typed languages. With dynamic systems, we get more power but also more error-prone places. There is no one solution. Each of type systems may be used in many different use cases, each of them may be used either correctly or not.

Here are some final rules you should keep in mind:

A type signature has to always tell the truth.

It can’t pretend it will always get you back what you expect.

A type system is your friend.

Use static types for domain modelling and documentation, not just type checking.

Resources