Functional programming with Swift 3
Functional programming with Swift 3.x
Disclaimer
This article assumes that you are familiar with the basics of Swift 3.x
Intro to functional programming
Functional programming is a software development paradigm that puts empahsis on immutable state, pure functions and declarative programming. If these terms mean nothing to you, don’t despair. We’ll cover all of them in this article. Since Swift has excellent functional programming support, it would really be a shame to ignore this part of the language and focus on pure object-oriented development. So in no particular order, here are some of the main features of functional programming.
1. Functions as first-class objects
This rule is the basis of every programming language. It means that functions are objects (citizens) just like any other value. It means we can assign functions to variables, pass them as function parameters, return them from other functions and do all other forms of data manipulation usually reserved for objects and primitives.
So for example - we can:
func bar() {
print("foo")
}
let baz = bar
baz() // prints "foo"
See how we passed that function just like any other object? We can also do something like this:
func foo() {
print("foo")
}
func bar() {
print("bar")
}
func baz() {
print("baz")
}
let functions = [
foo,
bar,
baz
]
for function in functions {
function() // runs all functions
}
Functions in an array?!? Yup! You can do lots of interesting things when functions are first-class citizens in your language.
We can also define a concept called
Anynonmous functions a.k.a Closures
They are just functions which do not have a name. They perform some action, but are usually needed only in one place - so we don’t bother naming them
The syntax for defining anonymous functions takes many forms - but in it’s most verbose form it’s this:
{(parameter1 : Type, parameter2: Type, ..., parameterN: Type) -> ReturnType in
}
They can be defined like this:
let anonymous = { (item: Int) -> Bool in
return item > 2
}
anonymous(3)
But this function has a name you might say! It’s clearly called anonymous
.
Did I not just claim that they had no name?
Well, yes I did. And it’s true. The anonymous function is only the code on the
right side of the =
operator. We’re just joining it to a variable - just
like we joined the bar
function earlier. We could have just done this:
({ (item: Int) -> Bool in
return item > 2
}(4)) // returns true
Now we just moved the right part of the previous expression and called the function with the number 4 right when we defined it. So as you can see - the function can be anonymous without any issues from the language. Anonymous function will be very useful later on - in the higher order functions chapter. Also it’s important to note that each and every function has it’s own type. The function type describes the input parameters and the return value. It looks like this:
let anon : (Int, Bool) -> String = { input,condition in
if condition { return String(input) }
return "Failed"
}
anon(12, true) // returns "12"
anon(12, false) // returns "Failed"
The part where we define the type is:
(Int, Bool) -> String
It means the function takes an Int
and a Bool
and returns a String
.
Since functions have types - the compiler enforces compile time type safety
This means that you can do this:
var functionArray = [(Int, Bool)->String]()
let one : (Int, Bool) -> String = { a, b in
if b { return "\(a)"}
else { return "Fail" }
}
functionArray.append(one)
let two = { (a: Int, b: Bool) -> String in
if b { return "\(a)" }
else { return "FailTwo" }
}
functionArray.append(two)
And it will work just fine. But if you try this:
let three : (Int)->Bool = { item in
return item > 2
}
functionArray.append(three)
You’ll get an error message saying:
ERROR at line 21, col 22: cannot convert value of type '(Int) -> Bool' to expected argument type '(Int, Bool) -> String'
functionArray.append(three)
The array type is (Int, Bool) -> String
and function three
has a
type (Int)->Bool
and they’re incompatible - just you couldn’t add a
String
to an Int
array.
The type syntax can sometimes be a little bit cumbersome, so Swift uses
a typealias
operator to shorten complex type declarations. We can shorten
our previously used declaration like this:
typealias Checker = (Int, Bool) -> String
var funcArray = [Checker]()
let one : Checker = { a, b in
if b { return "\(a)" }
return "Fail"
}
funcArray.append(one)
There is also an additional part of syntax sugar - Swift exposes the
function parameters as $0,$1,$2...$n
inside of the function.
This is very useful for writing one-liners. For example let’s rewrite the
one
function as a one-liner
let two: Checker = { return $1 ? "\($0)" : "Fail" }
The $0
is the first argument of type Int
and $1
is the second one
of type Bool
Also, if a closure is made of only one line - it returns that line by default So there is an even shorter way to write the funciton:
let three: Checker = { $1 ? "\($0)" : "Fail" }
Now thats some concise programming!
2. Pure functions
Pure functions are merely functions which do not depend on anything besides their function parameters. They do not change outside state, nor are affected by changes to the outside state. That’s it.
For example:
func countUp(currentCount: Int) -> Int {
return currentCount + 1
}
is a pure function. However,
var counter = 0
func countUp() -> Int{
counter += 1
return counter
}
is not a pure function. And that’s all there is to say about pure functions. They are a great way to make your code extremely testable and can save you a lot of time when things go south since they make debugging much easier. They are also great for parallel programming since each new call is depended only on the variables it created itself - thus avoiding all of the parallel/concurrent programming pitfalls
3. Higher order functions
Simply put, higher order functions are functions which take other functions as arguments and/or return other functions as a result. This simple concept lies at the core of functional programming. It enables us to create very useful abstractions and deal with the specifics during the call to the function - greatly reducting code doubling.
What does this mean practically?
For example - we might need to remove all elements from a sequence that are smaller than a certain number
An imperative approach would be:
func filter(sequence: [Int], elementsSmallerThan border: Int) -> [Int] {
var newSequence = [Int]()
for item in sequence {
if item < border {
newSequence.append(item)
}
}
return newSequence
}
filter(sequence: [1,2,3,4], elementsSmallerThan: 3) // returns [1,2]
Then later on - we might need to remove all the elements that are larger than a certain number
func filter(sequence: [Int], elementsLargerThan border: Int) -> [Int] {
var newSequence = [Int]()
for item in sequence {
if item > border {
newSequence.append(item)
}
}
return newSequence
}
filter(sequence: [1,2,3,4], elementsLargerThan: 2) // returns [3,4]
Now already, we have a lot of code doubling. Both times we need to go through a couple of steps:
- Create a new sequence
- Iterate over items
- Check the condition and append if it’s fulfilled
- Return the sequence
So how could higher-order functions help us?
They’ll allow us to create a single function which can take a condition as a parameter and filter out a sequence of integers any way we like
func filter(sequence: [Int], condition: (Int)->Bool) -> [Int] {
var newSequence = [Int]()
for item in sequence {
if condition(item) {
newSequence.append(item)
}
}
return newSequence
}
let sequence = [1,2,3,4]
let smaller = filter(sequence: sequence, condition: { item in
item < 3
})
let larger = filter(sequence: sequence, condition: { item in
item > 2
})
print(smaller) // prints [1,2]
print(larger) // prints [3,4]
Now we can filter out our sequence based on any condition we want! We removed the boilerplate code and our calls look more expressive than ever.
We can even leverage Swifts inbuilt syntax sugar for calling functions functions that are at the end of the argument list of another function
// We can ommit the parameter name and put the function as an
// anonymous function outside of the round brackets
let equalTo = filter(sequence: sequence) { item in item == 2}
// We can even ommit calling the parameters by name - if we
// use the "exposed" $0 variable which refers to the first function
// parameter
let isOne = filter(sequence: sequence) { $0 == 1}
In addition to putting functions as parameters, we can also return functions.
Let’s say we need to pick a function that modifies an Int
array based on some
condition:
typealias Modifier = ([Int]) -> [Int]
func chooseModifier(isHead: Bool) -> Modifier {
if isHead {
return { array in
Array(array.dropLast(1))
}
} else {
return { array in
[array[0]]
}
}
}
let head = chooseModifier(isHead: true)
let tail = chooseModifier(isHead: false)
Here we see how the chooseModifier
function returns one of the two
possible functions depending on the condition. Another great use is when
we need to get different variants of a same function.
Let’s define a function which checks weather a certain number is in some defined range:
typealias Range = (Int) -> Bool
func range(start: Int, end: Int) -> Range {
return { point in
return (point > start) && (point < end)
}
}
let fourToFifteen = range(start: 4, end: 15)
//Check if a number belongs to a range
print(fourToFifteen(14)) //true
print(fourToFifteen(16)) //false
We’ve defined the range
function as a function which returns another
function and that’s enabled us to spew various variants of the range
function without a large amount of effort.
When we’re talking about passing functions as parameters to other functions, one very important use case apperats:
Conditional parameter evaluation
So let’s say you have a parameter which may or may not be evaluated. You can’t be sure how the user will generate the parameter, but it might be computationally expensive to generate the value. You can make sure the parameter is only generated when it’s being used by taking advantage of higher-order functions. For example:
func stringify1(condition: Bool, parameter: Int) -> String {
if condition {
return "\(parameter)"
}
return "Fail"
}
func stringify2(condition: Bool, parameter: ()->Int) -> String{
if condition {
return "\(parameter())"
}
return "Fail"
}
let a = stringify1(condition: false, parameter: expensiveOperation())
let b = stringify2(condition: false, parameter: { return expensiveOperation()})
The first example is the usual way we pass parameters. The parameter is called every time the function is called, regardless of weather it’s being used.
The second version of the function optimizes for this behaviour by invoking a function that returns the value of the parameter only when it’s actually being used.
Now this behaviour is great, but a little bit messy to use. That’s why Swift has an inbuilt mechanism of handling these types of behaviour.
@autoclosure annotation
If we write the function to look like this:
func stringify3(condition: Bool, parameter: @autoclosure ()->Int) -> String {
if condition {
return "\(parameter())"
}
return "Fail"
}
where the @autoclosure
annotation is put before the type of the second
parameter - we can leverage the delayed (a.k.a. lazy ) loading functionallity,
while keeping the function call syntax of the normal function.
If we want to call the stringify3
function we do it like this:
stringify3(condition: true, parameter: 12)
Best of both worlds!
4. Currying
Function currying is a process in which we can take a function which takes n parameters and break it up into (at max) n functions which take one parameter. At that point - each function has a fraction of the responsibility of the entire “large” function. We can use that to break down complex functions into a set of simple functions. The most basic example of function currying is:
func add(_ a: Int) -> ((Int)-> Int) {
return { b in
return a + b
}
}
add(2)(3) // returns 5
Here we’ve defined a function add
which takes an Int
parameter and
returns a function which takes an Int
parameter and returns an Int
.
Then we called the function with the add(2)(3)
. This is not any
special syntax - it’s just a consequence of the fact that the return value
of the add(2)
function is a function which takes an integer.
Let’s say we do the following:
typealias Modifier = (String)->String
func uppercase() -> Modifier {
return { string in
return string.uppercased()
}
}
func removeLast() -> Modifier {
return { string in
return String(string.characters.dropLast())
}
}
func addSuffix(suffix: String) -> Modifier {
return { string in
return string + suffix
}
}
These are some functions which take a String and modify it. Now if we wanted to call them separately we could do it like this:
let uppercased = uppercase()("Value")
let removed = removeLast()(uppercased)
let withSuffix = addSuffix()(removed)
And with currying it looks like:
let a = addSuffix(suffix: "suffix")(removeLast()(uppercase()("IntitialFunction")))
Very short, but very readable and, honestly, quite confusing.
That’s why we can define a compose
function which takes two funcions and
composes them into a single one
func compose(_ left: @escaping Modifier, _ right: @escaping Modifier) -> Modifier {
return { string in
left(right(string))
}
}
Now we can make a function like this:
let a = compose(compose(uppercase(), removeLast()), addSuffix(suffix: "Abc"))("IntitialValue")
Now the function order and the bracket structure are much more clear, but it’s
still a lot of brackets. One way to solve this is to implement a currying operator,
but another way which I prefer is to wrap the Modifier
in a structure. After
doing that we get this code:
struct Modifier {
private let modifier: (String) -> String
init() {
self.modifier = { return $0 }
}
private init(modifier: @escaping (String)->String) {
self.modifier = modifier
}
var uppercase: Modifier {
return Modifier(modifier: { string in
self.modifier(string).uppercased()
})
}
var removeLast : Modifier {
return Modifier(modifier: { string in
return String(self.modifier(string).characters.dropLast())
})
}
func add(suffix: String) -> Modifier {
return Modifier(modifier: { string in
return self.modifier(string) + suffix
})
}
func modify(_ input: String) -> String {
return self.modifier(input)
}
}
// The call is now clean and clearly states which actions happen in
// which order
let modified = Modifier().uppercase.removeLast.add(suffix: "suffix").modify("InputValue")
print(modified)
So we’ve used functional programming to create a Modifier
for strings
which can now use arbitrarily complex modifications such as:
let modified = Modifier().add(suffix: "Initial").uppercase.add(suffix: "Later").removeLast.removeLast.removeLast.add(suffix: "Other").modify("Value")
Although these examples lack a dose of real-world usefulness - they are great at showing how functional programming can be used to build highly modular code.
The last example which uses struct
breaks the functional pattern a little
bit since it holds the private modifier
variable. It sacrifices some of the
safety for a little bit of syntactic sugar. The approach which would avoid all of this
would be to defin an infix
operator for currying the functions.
Then the entire thing could look something like this:
typealias Modifier = (String)->String
func uppercase() -> Modifier {
return { string in
return string.uppercased()
}
}
func removeLast() -> Modifier {
return { string in
return String(string.characters.dropLast())
}
}
func addSuffix(suffix: String) -> Modifier {
return { string in
return string + suffix
}
}
precedencegroup CurryPrecedence {
associativity: left
}
infix operator |> : CurryPrecedence
func |> ( left: @escaping Modifier, right: @escaping Modifier) -> Modifier {
return { string in
right(left(string))
}
}
let modified = uppercase() |> removeLast() |> addSuffix(suffix: "123")
print(modified("Abcd"))
This is the more functional approach since the functions are all pure and we keep the entire thing stateless.
Conclusion
Functional programming is a great way to improve the safety and testability of your code. But remember - it’s not a one-size-fits-all paradigm. In fact, no paradigm is. So use it when you need it, and combine it with the features you love in Swift to make better software.
Mislav Javor
I'm an entrepreneur and a software developer. CEO of a blockchain startup aiming to simplify buying and selling of electricity. Actively participating in the proliferation of healthy (technology oriented) blockchain culture. Organizer of Blockchain Development Meetup Zagreb, lectured at HUB385 Academy and University of Osijek on topics of smart comtract development. In my free time, I'm a singer and a guitar/piano player. Contact at mislav@ampnet.io