What's the Big Deal About Functional Programming?
(feat. Generics)
If you're an iOS engineer working with Swift, you've probably heard of the functional programming paradigm. This paradigm treats data processing as mathematical function evaluations without changing the state of the program. Actually, plenty of articles out there have already covered what functional programming is.
As an iOS engineer using Swift myself, I've been making an effort to apply the functional programming paradigm to my work. In this post, we'll gently skip over the "what is it" part and dive straight into how to actually use functional programming.
A Simple Problem
Let's start with a simple problem.
Given an array of integers, write a function that returns a new array where each element is incremented by 1.
If you were to write a function to solve this problem, it would look something like this:
func increment(array: [Int]) -> [Int] {
var result: [Int] = []
for x in array {
result.append(x + 1)
}
return result
}You create a new result array, increment each element of the input array by 1, and append it to the result array. Then, you just return this array at the end. It's a simple problem.
Here's the second problem.
Given an array of integers, write a function that returns a new array where each element is incremented by 2.
How should we solve this? Would it be okay to just add another function using the same method as above? Sure, from the perspective of getting the code to run, there's no problem.
But it's annoying. That's the core issue. Are we going to write new code every time this type of problem pops up? That feels pretty inefficient.
Fortunately, functional programming makes this much easier. First, we need to fix our initial code. If we borrow from the functional programming paradigm, it looks like this:
func compute(array: [Int], transform: (Int) -> Int) -> [Int] {
var result: [Int] = []
for x in array {
result.append(transform(x))
}
return result
}
func increment2(array: [Int]) -> [Int] {
return compute(array: array) { element in element + 1 }
}The compute function might look a bit unfamiliar. The second argument, transform, seems to play a big part in that awkwardness. The transform argument takes a function as its parameter. However, there's a constraint: it can only take a function that accepts an Int type argument and returns an Int type. We express a function with these conditions as an (Int) -> Int type function.
The part where we call compute inside increment2 also looks a little different. If we write this out the long way, it looks like the following:
func add(element: Int) -> Int {
return element + 1
}
func increment3(array: [Int]) -> [Int] {
return compute(array: array, transform: add)
}The add function takes an Int type argument, adds 1, and returns that value. Notice how we use this function as an argument inside our newly defined increment3 function. The increment2 function is just a more simplified way of writing the example above.
You can write increment2 even more concisely like this:
func increment2(array: [Int]) -> [Int] {
return compute(array: array) { $0 + 1 }
}Instead of defining a name for the argument, we used $0. This means it's the first argument of the function. The meaning—add 1 to the first argument—remains exactly the same. (If there are multiple arguments, you can express them as $1, $2...) Since we already defined the type of the first argument in the compute function, $0 is an Int type. In real projects, we usually write code like this. It's very convenient once you get used to it, so I highly recommend writing it this way.
Let's go back to our earlier question.
Given an array of integers, write a function that returns a new array where each element is incremented by 2.
Now, we can write a function to solve this problem like this:
func incrementTwo(array: [Int]) -> [Int] {
return compute(array: array) { $0 + 2 }
}Let's Use Generics
We implemented the compute function, passed the necessary function as an argument, and called it. Using this, we can perform various calculations with arrays. Now for the third problem.
Given an array of integers, write a function that returns a new array containing true when the element is even, and false when it's odd.
How should we write this? The returning array needs to be a [Bool] type. It seems like we could use our already-written compute function to write it like this:
func isEven(array: [Int]) -> [Bool] {
return compute(array: array) { $0 % 2 == 0 }
}Unfortunately, this code will throw an error. That's because the second argument is an (Int) -> Int type. Since { $0 % 2 == 0 } is an (Int) -> Bool type, an error occurs.
To solve this problem, we use a feature called generics. Generics allow us to create flexible functions or types that can work with various types by using a type parameter. It's a very useful feature. The genericCompute function below is a reimplementation of the compute function using generics.
func genericCompute<T>(array: [Int], transform: (Int) -> T) -> [T] {
var result: [T] = []
for x in array {
result.append(transform(x))
}
return result
}The T inside the <> is the type parameter. Depending on what we set T to, we can flexibly handle functions of various types. Using the genericCompute function, we can express the functions we've built so far like this:
func increment(array: [Int]) -> [Int] {
return genericCompute(array: array) { $0 + 1 }
}
func incrementTwo(array: [Int]) -> [Int] {
return genericCompute(array: array) { $0 + 2 }
}
func isEven(array: [Int]) -> [Bool] {
return genericCompute(array: array) { $0 % 2 == 0 }
}If you think about it a little more, there's no reason this function should only work on [Int], or integer arrays. Let's change this using generics, too.
func map<Element, T>(array: [Element], transform: (Element) -> T) -> [T] {
var result: [T] = []
for x in array {
result.append(transform(x))
}
return result
}Actually...
Does seeing the new function name map feel somewhat familiar? That's right. We've just implemented the map function from the Swift Standard Library. Actually, to be more precise, it's structured like this:
extension Array {
func map<T>(_ transform: (Element) -> T) -> [T] {
var result: [T] = []
for x in self {
result.append(transform(x))
}
return result
}
}Since Array already has Element as a type parameter, we only needed to add a new T. In practice, you can use it like this:
let array = [1, 2, 3, 4]
array.map { $0 + 1 }
array.map { $0 + 2 }
array.map { $0 % 2 == 0 }As I mentioned above, map is already implemented in the Swift Standard Library. So, you don't actually need to implement it yourself. The important takeaway is that map is also built using a functional programming approach, and we are already using the functional programming paradigm.
Up to this point, we've looked at how to utilize the functional programming paradigm and use generics while implementing map from scratch. Next time, I'll prepare an even more interesting Swift-related post. Thanks for reading all the way to the end!
Smis, Swift man in Seoul
I'd like to wrap up this post by expressing my infinite gratitude to DelightRoom for providing the opportunity to study the functional programming paradigm and apply it to actual code.
If you want an environment where you can grow as an iOS engineer! Check out the Alarmy team
References
Chris Eidhof. 'Functional Swift.' Apple Books.
Yagom. 'Swift Programming 3rd Edition.' Hanbit Media.