DEV Community

Balraj Singh
Balraj Singh

Posted on

Function Composition

Continuing from our last session on Side-effect we will continue our journey to explore the core concepts of functional programming. The next stop in our journey is at Composition. One of the most important concepts of programming for any kind of programming style.

Today we answer 3 basic questions regarding Composition.

  1. What is composition?
  2. Why is composition required?
  3. How to compose a function?

What is Composition?

Composition is the process of combining two or more smaller components to produce a new larger component. The word component has different meaning in different context. In functional programming, components are functions. These functions compose to new function.
Put simply, a composition of functions f and g can be defined as f(g(x)), which evaluates from the inside out — right to left. In other words, the evaluation order is:
x
g
f

Why is composition required?

The answer to the above question starts by asking How do we solve problems?
We decompose bigger problems into smaller problems. If the smaller problems are still too big, we decompose them further, and so on. Finally, we write code that solves all the small problems. And then comes the concept of composition, we compose those pieces of code to create solutions to larger problems.

I will argue strongly that composition is the essence of programming.
We’ve been composing things forever, long before some great engineer came up with the idea of a subroutine.
Some time ago the principles of structural programming revolutionized programming because they made blocks of code composable.
Then came object oriented programming, which is all about composing objects.
Functional programming is not only about composing functions and algebraic data structures — it makes concurrency composable — something that’s virtually impossible with other programming paradigms.

Why do we do what we do? This process of hierarchical decomposition and composition is not imposed on us by computers.

Lets understand this by taking an example. I will present before you 2 pieces of code we need to check which piece of code we can understand better, faster and easily.

**Code 1:**
let gradientView = GradientView()
 gradientView.fromColor = UIColor(red: 0.5, green: 0.85, blue: 1, alpha: 0.85)
 gradientView.toColor = .white
 gradientView.translatesAutoresizingMaskIntoConstraints = false

let logoImageView = UIImageView(image: UIImage(named: “logo”))
 logoImageView.widthAnchor.constraint(equalTo: logoImageView.heightAnchor, multiplier: logoImageView.frame.width / logoImageView.frame.height).isActive = true

let gitHubButton = UIButton(type: .system)
 gitHubButton.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
 gitHubButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
 gitHubButton.clipsToBounds = true
 gitHubButton.layer.cornerRadius = 6
 gitHubButton.backgroundColor = .black
 gitHubButton.tintColor = .white
 gitHubButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16)
 gitHubButton.setImage(UIImage(named: “github”), for: .normal)
 gitHubButton.setTitle(“Sign in with GitHub”, for: .normal)

let orLabel = UILabel()
 orLabel.font = .systemFont(ofSize: 14, weight: .medium)
 orLabel.textAlignment = .center
 orLabel.textColor = UIColor(white: 0.625, alpha: 1)
 orLabel.text = “or”

let emailField = UITextField()
 emailField.clipsToBounds = true
 emailField.layer.cornerRadius = 6
 emailField.layer.borderColor = UIColor(white: 0.75, alpha: 1).cgColor
 emailField.layer.borderWidth = 1
 emailField.borderStyle = .roundedRect
 emailField.heightAnchor.constraint(equalToConstant: 44).isActive = true
 emailField.keyboardType = .emailAddress
 emailField.placeholder = “blob@gmail.com”

let passwordField = UITextField()
 passwordField.clipsToBounds = true
 passwordField.layer.cornerRadius = 6
 passwordField.layer.borderColor = UIColor(white: 0.75, alpha: 1).cgColor
 passwordField.layer.borderWidth = 1
 passwordField.borderStyle = .roundedRect
 passwordField.heightAnchor.constraint(equalToConstant: 44).isActive = true
 passwordField.isSecureTextEntry = true
 passwordField.placeholder = “••••••••••••••••”

let signInButton = UIButton(type: .system)
 signInButton.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
 signInButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
 signInButton.clipsToBounds = true
 signInButton.layer.cornerRadius = 6
 signInButton.layer.borderColor = UIColor.black.cgColor
 signInButton.layer.borderWidth = 2
 signInButton.setTitleColor(.black, for: .normal)
 signInButton.setTitle(“Sign in”, for: .normal)

**Code 2:**
let gradientView = GradientView()
 gradientStyle(gradientView)

let logoImageView = UIImageView(image: UIImage(named: “logo”))
 implicitAspectRatioStyle(logoImageView)

baseButtonStyle(.appearance())

let gitHubButton = UIButton(type: .system)
 gitHubButton.setTitle(“Sign in with GitHub”, for: .normal)
 gitHubButtonStyle(gitHubButton)

let orLabel = UILabel()
 orLabelStyle(orLabel)
 orLabel.text = “or”

let emailField = UITextField()
 emailTextFieldStyle(emailField)

let passwordField = UITextField()
 passwordTextFieldStyle(passwordField)

let signInButton = UIButton(type: .system)
 signInButton.setTitle(“Sign in”, for: .normal)
 borderButtonStyle(signInButton)
Enter fullscreen mode Exit fullscreen mode

The above Code1 and Code2 are creating a piece of UI via code. Code1 has written code in a very imperative style with each steps exactly defining how to apply style. This makes the code look complicated and harder to understand for a fresh piece of eyes. On the other hand Code2 uses some pre-built functions to apply style. This makes code more readable, reusable, easy to understand.

The above example reflects the limitations of the human mind. Our brains can only deal with a small number of concepts at a time. One of the most cited papers in psychology, The Magical Number Seven, Plus or Minus Two, postulated that we can only keep 7 ± 2 **“chunks**” of information in our minds. The details of our understanding of the human short-term memory might be changing, but we know for sure that it’s limited. The bottom line is that we are unable to deal with the soup of objects or the spaghetti of code. We need structure not because well-structured programs are pleasant to look at, but because otherwise our brains can’t process them efficiently. We often describe some piece of code as elegant or beautiful, but what we really mean is that it’s easy to process by our limited human minds. Elegant code creates chunks that are just the right size and come in just the right number for our mental digestive system to assimilate them.

With this we can surely say that composition is one the key concepts of modern day programming.

How to compose?

To understand function composition concept we will start with a very trivial example.

Let’s define a function. We can define an increment function that takes an Int and returns an Int:

func incr(_ x: Int) -> Int {
 return x + 1
}
Enter fullscreen mode Exit fullscreen mode

To call our function, we pass a value to it.

incr(2) // 3
Enter fullscreen mode Exit fullscreen mode

Now let’s define a square function to square an integer:

func square(_ x: Int) -> Int {
 return x * x
}
Enter fullscreen mode Exit fullscreen mode

We can call it in a similar fashion:

square(2) // 4
Enter fullscreen mode Exit fullscreen mode

We can even nest our function calls. In order to first increment, then square a value:

square(incr(2)) // 9
Enter fullscreen mode Exit fullscreen mode

We can also write the above functions as an extension on Int type as follows:-

extension Int {
 func incr() -> Int {
 return self + 1
 }

func square() -> Int {
 return self * self
 }
}
Enter fullscreen mode Exit fullscreen mode

To use our incr method, we can call it directly:

2.incr() // 3
Enter fullscreen mode Exit fullscreen mode

And we can square the result by chaining our method calls:

2.incr().square() // 9
Enter fullscreen mode Exit fullscreen mode

This is more readable than the earlier version of implementation. The above example reads nicely from left-to-right whereas free functions read from inside-out, and it takes more mental work to see that we call incr before we call square. We can imagine that a more complicated nest of function calls would be all the more difficult to unpack.

Composition Operators

*Operator |> *
There are many programming languages that uses free functions and but retain this kind of readability by using an infix operator for function application. Using Swift Language we can define an infix operator so that we can compose free functions.
Here we’re defining a “pipe-forward” operator. It’s based on prior art: F#, Elixir, and Elm all use this operator for function application.

precedencegroup ForwardApplication {
 associativity: left
}

infix operator |>: ForwardApplication

func |> <A, B>(a: A, f: (A) -> B) -> B {
 return f(a)
}
Enter fullscreen mode Exit fullscreen mode

It’s generic over two types: A and B. The lefthand side is our value, of type A, while the righthand side is a function from A to B. We finally return B by applying our value to our function.
Now we can also define the above example as follows:-

2 |> incr |> square // 9
Enter fullscreen mode Exit fullscreen mode

This looks a lot like our earlier method version:

2.incr().square() // 9
Enter fullscreen mode Exit fullscreen mode

The pipe operator is used to apply the left side value to the right side function. Using this we cannot create a new function. It can only be used to apply a left value to right. Hence doing incr |> square is not going to produce a new function.

Now keeping that in mind lets create a new function with a cool operator which can help us to compose new function from combing 2 smaller functions.

*Operator >>> *
This operator is another standard operator used in many languages like Haskell to compose 2 functions into 1 new function.
Lets see its implementation:-

precedencegroup ForwardComposition {
 associativity: left
 higherThan: ForwardApplication
}

infix operator >>>: ForwardComposition

func >>> <A, B, C>(f: [@escaping](http://twitter.com/escaping) (A) -> B, g: [@escaping](http://twitter.com/escaping) (B) -> C) -> ((A) -> C) {
 return { a in
 g(f(a))
 }
}
Enter fullscreen mode Exit fullscreen mode

It’s a function that’s generic over three generic parameters: A, B, and C. It takes two functions, one from A to B, and one from B to C, and glues them together by returning a new function that passes the value in A to the function that takes A, and passing the result, B, to the function that takes B.

Now we can take our incr function and forward-compose it into our square function:

incr >>> square
Enter fullscreen mode Exit fullscreen mode

With this we have a whole new function (Int) -> Int. This function first increment the value then squares it.

We can use this new function in combination with the above pipe operator to apply a value as follows:-

2 |> incr >>> square
Enter fullscreen mode Exit fullscreen mode

This is similar to 2.incr().square() // 9 but has an added advantage of having a new composed function incr >>> square and making more readable with the new pipe operator |>.

With these two new operators we now have the ability to compose two functions into a new function and the apply a value to it using a pipe operator.

Lets now see few advantages that we have gained following the above approach of composing functions.

Advantages of Composition

How can we compose these functions in traditional way. If we want to combine incr and square functionality we have no choice but to extend Int and write another function that compose these 2 functionality.

extension Int {
    func incrAndSquare() -> Int {
        return self.incr().square()
    }
}
Enter fullscreen mode Exit fullscreen mode

In use, we can call the new method on a value:

2.incrAndSquare() // 9
Enter fullscreen mode Exit fullscreen mode

This works but there is lot of boiler plate and noise to achieve this. Plus if we want to reverse this i.e. first square and then increment we need to write a new function again. When composition takes this much boilerplate and effort, we have to ask ourselves: is it even worth it?

Meanwhile, function composition is a bite-sized piece that is wholly intact without any noise:

incr >>> square
Or 
square >>> incr
Enter fullscreen mode Exit fullscreen mode

You can further see the reuse by looking at the smallest valid components. If we delete parts of function composition and application, we still have a valid program.

2 |> incr >>> square
// every composed unit still compiles:
2 |> incr
2 |> square
incr >>> square
Enter fullscreen mode Exit fullscreen mode

In Swift initializers are at our disposal for function composition. For example, we can take the above new composed function and combine it with String init to convert the above value to a string type:-

incr >>> square >>> String.init
// (Int) -> String 
Enter fullscreen mode Exit fullscreen mode

And we can pipe a value through to produce a string result.

2 |> incr >>> square >>> String.init // “9”
Enter fullscreen mode Exit fullscreen mode

Meanwhile, in the method world, we can’t chain the result along to our initializer. We need to change the order in which we read things by wrapping the initializer around the methods.

String(2.incr().square())
Enter fullscreen mode Exit fullscreen mode

We can also use the above function composition with map function of an Array like:-

[1, 2, 3].map(incr >>> square) // [4, 9, 16]
Enter fullscreen mode Exit fullscreen mode

Now this starts to look cool!!! Our composition has seamlessly integrated with map function also.

Conclusion

With this we can now start seeing how composition helps us to write better readable, reusable and concise code. This sets the basics of function composition. In our upcoming chapters we will see how to use this concept in real world scenarios. Which will be even cooler!!!
So stay tuned…

Top comments (0)