Posts Interfaces vs Pure Functions

Interfaces vs Pure Functions

My argument laid out

Coding to an interface is not very useful, instead, we should code to a pure function. By doing this, your code will be easier to read/maintain. It will be self documenting. Productivity will improve, as you don’t need to read so much code. Interfaces should be reserved for external depencies.

For a function to be pure it must honour some promises and those promises give us confidence that the business logic we write, will execute correctly. By using interfaces (a function could also be used) for external dependencies we can use mocks, to test our logic (we’re not try to test the external dependency). Business logic should be written with pure functions.`

A quick recap of interfaces

I expect that you already know what an interface is, so this will be brief.

An interface is meant to be a contract of sorts. ‘Code to an interface’ is what we’re all taught. We are sold on the idea that all we need is an interface to make our code work.

In F#, the language enforces this. Once you define an instance/object to be of a given interface, you can’t access any other methods on it - the way it should be. In contrast, C#/Java do not make assertions. Given an instance that implements some interface, the languages allow the developer to call either methods on the class or methods on the interface. Here a simple code snippet to demonstrate how this works in F#:

type IFoo = 
    abstract member Foo: int -> int

type Foo() = 
    member this.Bar a = a
    interface IFoo with 
        member this.Foo i = i    

let x = Foo()
let y = Foo() :> IFoo
x.Foo 42 // won't compile
y.Bar 42 // Won't compile

x.Bar 42 // happy path
y.Foo 42 // happy path

The two images below show the intellisense results:

Intellisense dialog showing that the interface methods are not available on the class
Intellisense dialog showing that the class methods are not available on the interface

F#, a functional first programming language, does OOP better than C#/Java!`

What is an interface missing?

Let’s assume that we know how to code to an interface. The challenge is that it does not tell us enough to actually code against it. Consider the following:

type IOperation = 
    abstract member Run: int -> int -> int 

The above defines an interface IOperation that has one method Run. The Run method takes in two integers and returns an integer. Clearly, the naming is terrible here, and that is the point. Naming is always hard, and once published it is even harder to change. Instead of spending all our time thinking of the ‘right’ name (naming is subjective), what can we do instead is make the code easier to understand (good naming should still be followed).

Considering the IOperation interface, all that we know is that it appears to be some kind of math operation. In reality, though, we really have no idea. If the interface is implemented in our code-base we could go have a look at what it does, which somewhat defeats the purpose of coding to an interface. Additionally there is no proof that the method uses the inputs. There is no proof that the return integer is useful as well. Does it throw exceptions?

There an alternative!`

What is a pure function

A ‘pure function’ is the cornerstone of functional programming. The concept is rather simple, given the same inputs, the same output must be returned. For a ‘total pure function’, the function guarantees that for every range of possible inputs there will be an output of the same type. Total pure functions are the smallest set. Pure functions include total functions and additional functions. Finally, we have just functions (a function attached to a class is a method) which can do anything.

Very few languages can type check that a function is a total pure function. Some languages such as Haskell and PureScript can type check that a function is pure. While the rest of the languages (including F#) require the developer to honour that the function is pure.

Here is an example of a total pure function

foo: int -> int -> int

Consider this function, which is similar to our interface. Given we know it is a total pure function, we can make some intelligent deductions about the function.`

Eliminating possibilities

What it can’t do:

  • can’t talk to the web (that might fail)
  • can’t talk to a DB (that might fail)
  • can’t be random - the answer must be the same
  • can’t throw exceptions
  • if the function is total then every int is in the valid range

This has eliminated many possibilities. It suggests that the function must be math based on some form. It could be +, - or * (a poor implementation could ignore one of the inputs, eg the identity function and still be a total pure function. I am assuming that as developers we’re attempting to write quality code.) If we know it to be a total pure function then it can’t be divide as that would fail with x/0. Coding to this function is going to be a lot easier than the interface we had before, and we don’t even have a name. The code we write for this function will execute without exceptions.

Total pure functions provide the highest level of information to code against. Pure functions are the next best, but in some cases, the input may not be valid. For example, if we know that foo was only a pure function (not total), then divide could be a valid implementation. Many cases are valid, but one case is not, x/0. It’s possible to convert to convert some pure functions into total pure functions.

Pure functions are what gives FP its reputation ‘if it compiles it works’.`

Not all errors are the same

Now just blindly coding to this function will not produce a correct program with regards to the acceptance criteria (or the end user). If the function should be have been add, but we used subtract then our application is wrong. Regardless of whether we use an interface or pure function, both applications will suffer from this error (and I don’t believe it to be a coding issue either). A tester, TDD and BDD automated testing will help with these errors.

Pure functions help first and foremost by eliminating exceptions from our business logic. Pure functions also reduce the size of possible functions (self-documenting code through types), giving a greater chance of choosing the right pure function. It is these advantages that make using a pure function much more compelling over an interface.`

Pure function provide more information than an interface

Coding to a [total] pure function gives us confidence that our software won’t throw exceptions in the pure functions.

In this post, I intentionally left off meaningful names. When we put meaningful names back in, the risk of choosing the wrong pure function goes down considerably. The challenge is simply selecting the most meaningful name from a set of functions that fit the type signature.

Naming the function is also much easier as there are less subtle differences between each of the functions. Naming is now a process of following domain driven design (DDD), so each of the names will be well-defined. Types can also be used to control the input, and these too will part of bounded-context under DDD.

Considering our example above, if we must code to that pure function, then the set of functions that we could provide are limited to some of the following [add, subtract, minus, pow].

Even if these functions (foo:int -> int -> int) were not math functions, but business logic functions, the name plus the knowledge that they are pure functions allows us to make intelligent assumptions around what they will do. The same can not be said for interfaces. As already stated, with interfaces we do not know anything about the relationship of inputs to outputs. We don’t know if it will fail either (FP has types that represent the failure case in the type system).`

Creating documentation types

Meaningful names can also be extended to the types as well. In most FP first languages, creating types (classes) is very easy and only a few lines of code (not an entirely new file).

Creating additional types to describe our domain (business logic) can convert pure functions to total functions. This improves the chances of our application being correct, as it removes possible invalid function. It also helps document our code with types (less documentation required).

Covering this in more detail is the subject of another blog post.`

Productivity boost - fixing errors

By coding to a pure function, rather than an interface, errors are faster to fix (an error is when the application does the wrong thing, i.e. not meeting the acceptance criteria). Because the interface makes very little guarantees, the developer will most likely need to debug the code in the interface. This will includes exceptions and coding errors as well as not meeting the acceptance criteria.

The pure function, however, guarantees that it will not throw exceptions, so needs to only look at his/her own code. Typically either the inputs to function were wrong, or the wrong function was used.

This reduces the search time to find the bug, improving developer productivity.`

Pure functions in the real world

To prove a function is a total pure function, property-based testing (PBT) must be used. PBT is the process of using randomness and properties (characteristics of a function that describe the relationship of inputs to outputs) to test functions. The randomness attempts to verify that the function is valid under all possible inputs. PBT is beyond the scope of this function (leave a comment if you want more information).

Haskell is the most well known (and used) language that provides compile checking for pure functions. For this reason, Haskell makes for a great learning language to prove that the function is pure. (You could consider using Haskell in production if you’re writing a backend).

For many other languages, F# included, practice and care will need to be taken when writing functions. After sufficient practice, it is easy to create applications with pure functions, and the need for the type level checking (eg learning with Haskell) is not required so much.

Write your business logic using pure functions. Model external dependencies using interfaces. Your program will be well structured according to the SOLID principles.`

Taking action

Consider writing some Haskell in your spare time. Write the business/core logic of the application in a function. Be sure to write out the type signature and leave out IO from the type signature. This will teach you how to identify a pure function and feedback (from compiler) will be given if you are wrong.

Change to an FP first language that is suitable for your domain - F# (my personal favourite), Kotlin, TypeScript or Rust should cover most cases. These languages prefer a functional style so will make it easier to code with pure functions. Languages such as C#, Java, C++ can use methods are pure functions (LINQ in C#, streaming API in Java 8) but the code will be very verbose and not very idiomatic.

Educate your co-workers and managers on the benefits of pure functions. Code reviews are a great opportunity for this.

Happy [type] safe coding


This post is licensed under CC BY 4.0 by the author.