profile picture

Construction by composition

In mathematics, functions are composable because for a given input they are guaranteed to return the same output. By composing simple functions, complex behavior can be modeled in a way that is straight forward to reason about. In most programming languages however, functions do not compose well because they can perform arbitrary side-effects, like reading the system clock, possibly resulting in different outputs for the same input.

For this reason many functions are hard to test, hard to reuse, and a source of unexpected bugs. Take for example the equals method of the URL class in Java.

public boolean equals(Object obj)

Judging from the function signature it would be reasonable to assume that two URLs can be compared for equality without much thinking. Reading through the function documentation, we learn that the function for some reason tries to resolve the hostnames to IP addresses which is a blocking operation. Furthermore, in certain circumstances the behaviour of equals is even inconsistent with the documentation. Imagine trying to hunt down a bug deep in a production system that originates from the unintended domain name resolution while comparing URLs. It is understandable why complex systems are generally not built by composing poorly behaving functions.

Functions that do not perform any side-effects are called pure functions. They are not allowed to depend on global variables, read or write files, open network sockets, throw exceptions or do anything that breaks the assumption that for a given input they always return the same output. From a developer’s perspective that makes them composable but useless for interacting with the real world. Like we saw earlier it can be challenging to differentiate between pure and impure functions, which further reduces their usefulness as reusable building blocks.

The result is that functions are mostly used for control flow and as a method to break the program into smaller units.

Haskell takes a completely different approach. All regular functions are pure and are composed by using the composition function (.). If f is a function that maps inputs from a to b, and g maps inputs from b to c, then function h maps inputs from a to c by first applying f and then g.

f :: a -> b
g :: b -> c

h :: a -> c
h = g . f

Any function that may perform side-effects must be tagged with IO in the return type. Consider getLine that reads a string from the standard input and returns it or putStrLn that prints a string to the standard output.

getLine :: IO String
putStrLn :: String -> IO ()

The complexity is not gone but rather made explicit in the type system and by encouraging the use of pure functions whenever possible the surface area of unintended side-effects is dramatically reduced.

readLn :: Bool -> IO String
readLn b = 
 if b 
  then getLine 
  else return "boolean is false"

Much like pure functions, functions tagged with IO in the return type can be composed using Kleisli composition (<=<). Therefore IO tagged functions are ideal building blocks for more complex programs in Haskell.

echo :: Bool -> IO ()
echo = putStrLn <=< readLn

@code #haskell