lastminute.com logo

Technology

A matter of purity

angelo_sciarra
angelo sciarra

What are pure functions? Why should you even care?


alt Graal
Searching for the Graal of FP: purity

Hello folks, for this post I’ve chosen a more philosophical topic: pure functions. I ended my previous post talking about the concept of functions in FP, saying that:

functions in FP are just mapping between inputs and outputs (and nothing more)

This is an informal definition of pure functions. Other definitions you may have heard are:

  • pure functions are functions in the mathematical sense
  • pure functions are functions that will return the same output anytime they are given the same input
  • pure functions are functions without side effects

The mathematical definition of a function is the following:

Given two non-empty sets X and Y, a function f from X to Y is a relation such that for all x in X there exists only one y in Y such that (x, y) belongs to f, or, said otherwise, such that y = f (x)

(note the for all is crucial because what we are looking for are total functions)

So, why should you care about pure functions? Well, the reason is that the use of pure functions will help you write correct programs. How? (you might be wondering) Let’s see.

Local reasoning

You may have heard about local reasoning or not. What it refers to is the ability to reason about what a piece of code does in isolation, meaning you don’t need any knowledge about the context in which said piece of code is executed to understand what it does. How pure functions relate to local reasoning? Well, if a function takes care only of transforming an input value in an output one, without relying on any other external piece of information, that function enables of course local reasoning about what it is doing. Let’s look at an example of a function that does not allow local reasoning:

var counter = 1;

fun `not easy to reason about`(anInt: Int) =
	if (counter < 5) {
		counter = counter + 1
		anInt * 2
	} else {
		counter = counter + 1
		anInt * 3
	}
}

Why is that? Because the logic of the function relies on the mutable, global counter variable. The same example can be translated into a more familiar setting for those of you who are more accustomed to Java and OOP (instead of Kotlin)

public class TheOne {

	private int counter = 0;

	public notEasyToReasonAbout(int anInt) {
		int result;
		if (counter < 5) {
			result = anInt * 2;
		} else {
			result = anInt * 3;
		}
		counter = counter + 1;

		return result;
	}

}

I guess you have seen many classes like TheOne shown here. What is the problem? Again the fact the method notEasyToReasonAbout is relying on the value of the mutable counter that is out of its scope (this time is not global, but still has a lifespan that goes beyond the method execution, and it can mutate over it).

DISCLAIMER

I am not saying that OOP way of doing things is intrinsically impure. It is just that languages like Java tend to make it easier to use constructs that work against local reasoning (like mutability) while others (think of modern languages like Kotlin, or Scala) choose to make it easier to use constructs that make it easier to apply local reasoning (examples are the val keyword that introduces immutable references, the default choice of immutable collections over mutable ones and so on).

Referential transparency

Once you start adopting pure functions in your codebase you gain another capability you were missing before, referential transparency. What is it about? Say you have a piece of code like

val x = aFunction(y)

If aFunction is a pure function you gain the ability to safely replace any occurrence of aFunction(y), provided that y is immutable, with x, because x will always be the result of applying aFunction to y. Have you ever heard of temporal coupling? Well if you use pure functions you can forget about it and move around your values as you please.

Do you think the following function is referentially transparent?

fun `read file lines`(path: String): List<String> =
	File(path).readLines()

No. The reason is that anytime you call this function, even with the same path as input, you are not sure you will get the same results. Reasons may be

  • the file doesn’t exist anymore (then you’ll get an exception)
  • the file has been modified (so the content will differ)
  • the file permissions have been changed and you no longer have read permission (once again you’ll get an exception)

You may be wondering how can one write pure functions that do things like IO but I won’t talk about this topic right now (you will have to wait for a new article emoji-smirk).

Function composition

The beauty of pure functions is that you can compose them or, going the other way around, you can split a big function into pieces and then compose them together to obtain the initial function.

If you think about it, writing a program to solve a need can be abstracted to writing one big function that turns our program input in its outputs.

As usual, the approach one takes is: divide et impera! But what’s the point of splitting a problem in subproblems if you cannot glue back the solutions to the subproblems into an overall solution?

With pure functions, function compositions is a matter of having matching types. Given f: B -> C and g: A -> B, they can be composed to obtain h: A -> C, because the output type of g matches the input type of f, so h = f . g

Conclusion

Once you start using pure functions, you will start to think more and more in terms of types and of functions as mappings between types.

Your workflows/use cases (whatever you are calling them) will naturally emerge from the types of the functions you have developed to solve the smaller bits of your problem domain.

Follow the types!

You will end up doing TDD: Types Driven Development!

Originally posted on https://dev.to/eureka84/a-matter-of-purity-2flj


Read next

SwiftUI and the Text concatenations super powers

SwiftUI and the Text concatenations super powers

fabrizio_duroni
fabrizio duroni
marco_de_lucchi
marco de lucchi

Do you need a way to compose beautiful text with images and custom font like you are used with Attributed String. The Text component has everything we need to create some sort of 'attributed text' directly in SwiftUI. Let's go!!! [...]

A Monorepo Experiment: reuniting a JVM-based codebase

A Monorepo Experiment: reuniting a JVM-based codebase

luigi_noto
luigi noto

Continuing the Monorepo exploration series, we’ll see in action a real-life example of a monorepo for JVM-based languages, implemented with Maven, that runs in continuous integration. The experiment of reuniting a codebase of ~700K lines of code from many projects and shared libraries, into a single repository. [...]