Functional programming might be a strange beast for seasoned Cocoa veterans. It is a completely different approach to what’s contained in Apple code samples. It requires practice to structure the code in a functional way, but it has some very nice benefits attached to it:
- Thanks to no side-effects and extensive immutable data structures usage testing & eventual debugging are very easy.
- The code becomes more expressive and illustrates what we want to achieve more instead of how do we get there.
- We split the codebase in very small, reusable methods.
If you feel like achieving any of these would help your team build the product, you should definitely try functional programming and see whether it suits you or not.
So, what is functional programming?
Functional programming is all about free functions, like this one little fella:
func isInFuture(_ currentDate: Date)
-> (Date) -> Bool {
return { $0 > currentDate }
}
Our little fella, provided a current date, can give us a recipe to say whether another date is in future or not. It has a pretty odd signature, though. How should we use it? Let’s have a test case to illustrate how to call the function:
class DateTests: XCTestCase {
var sut: ((Date) -> Bool)!
override func setUp() {
super.setUp()
// date(at:) is a helper that creates a date based on
// yyyy-MM-dd HH:mm:ss formatted string
self.sut = isInFuture(date(at: "2018-10-13 15:30:10"))
}
func testIsInFutureReturnsCorrectValues() {
XCTAssertTrue(sut(date(at: "2018-10-13 15:30:11")))
XCTAssertFalse(sut(date(at: "2018-10-13 15:30:10")))
XCTAssertFalse(sut(date(at: "2018-10-13 14:00:00")))
}
}
Which means our method is called like:
isInFuture(Date())(Date())
// (Date) -> (Date) -> Bool
We are not used to call functions this way in object-oriented world. Instead of calling the function twice passing single date each time we would just pass 2 arguments directly. What's the difference?
Function composition
Functional programming is all about processing the data using a set of very small composable units. Having a (Date) -> (Date) -> Bool
signature would not make any sense, if we already had a date and would like to check whether it is in future or not. But instead, let’s imagine that all we have is a timestamp.
let timestamp: TimeInterval = 1539444610.0
// Date(timeIntervalSince1970: timestamp)
// == date(at: "2018-10-13 15:30:10")
Now we would like to know whether this timestamp is in the future. We want to build a function with following signature:
let isFutureTimestamp: (TimeInterval) -> Bool
It should be pretty easy to achieve. We have a date initialiser that can take a time interval and we have a method to test whether date is in future:
Date(timeIntervalSince1970:) // (TimeInterval) -> Date
isInFuture(Date()) // (Date) -> Bool
Looking at the signatures, the output type from the 1st function matches the input type on a 2nd function which means we could chain them together to get our desired (TimeInterval) -> Bool
signature.
Such chaining is called composition and it’s a universal maths law. Given 2 functions with matching output to input type, we can always compose them into a function that goes from the 1st function input to 2nd function output:
func comp<A, B, C>(
_ f: @escaping (A) -> B,
_ g: @escaping (B) -> C)
-> (A) -> C {
return { a in g(f(a)) }
}
With our brand new comp
function we can now declare isFutureTimestamp
easily:
let isFutureTimestamp: (TimeInterval) -> Bool =
comp(Date(timeIntervalSince1970:), isInFuture(Date())
Let’s verify whether it behaves as expected:
class IsFutureTimestampTests: XCTestCase {
var sut: ((TimeInterval) -> Bool)!
override func setUp() {
super.setUp()
let currentDate = Date(timeIntervalSince1970: 10_200_300)
sut = comp(Date.init(timeIntervalSince1970:),
isInFuture(currentDate))
}
func testFutureIsCorrectlyDetermined() {
XCTAssertTrue(sut(10_200_301))
XCTAssertFalse(sut(10_200_300))
XCTAssertFalse(sut(10_200_200))
}
}
The tests are green, hooray!
Having composed the new function, we can now answer our original question. What benefit did (Date) -> (Date) -> Bool
shape of isInFuture
function had?
It turns out that it enabled us to separate configuration from the processed data. It’s a functional analogy to the dependency injection pattern. In our case, the current date was just a configuration that we could partially apply and answer the question: is the input date in future from now:
let isInFutureFromNow: (Date) -> Bool = isInFuture(Date())
Meanwhile the second Date
argument was an actual data that we have been processing. Different dates would yield us different results with the same configuration.
Operator composition
Having to type:
comp(Date.init(timeIntervalSince1970:), isInFuture(Date()))
to compose 2 functions might not be particularly convenient. First, we have a lot of brackets to scan through with our eyes. It’s hard to visually pair the last closing bracket with the opening one located near the comp
function name. Second, splitting the code into multiple lines does not make it very beautiful either:
let isFutureTimestamp: (TimeInterval) -> Bool = comp(
Date.init(timeIntervalSince1970:),
isInFuture(Date())
)
It would look way nicer if we could remove the outer brackets and stick function name in between the arguments:
Date.init(timeIntervalSince1970:) comp isInFuture(Date())
Unfortunately, Swift won’t allow us to use this syntax. Fortunately, we can get very close by defining a custom operator:
infix operator >>>
func >>> <A, B, C>(
_ f: @escaping (A) -> B,
_ g: @escaping (B) -> C)
-> (A) -> C {
return comp(f, g)
}
Using the operator we can now refactor the isFutureTimestamp
method:
let isFutureTimestamp: (TimeInterval) -> Bool =
Date.init(timeIntervalSince1970:)
>>> isInFuture(Date())
The composition reads very nice right now. Composed parts are visually distinct. Splitting the composition into separate lines looks very nice.
Composing model transformations
That’s a lot of words already and you might be wondering:
Why should I even bother?
That’s a valid question! So far we have only defined isInFuture
function that’s very straightforward. We’ve also composed another one (isFutureTimestamp
) that’s not too complicated either. At best we could do the following:
let dates: [Date] = // ...
let futureDates = dates.filter(isInFuture(Date()))
// or
let timestamps: [TimeInterval] = // ...
let futureTimestamps = timestamps.filter(
Date.init(timeIntervalSince1970:) >>> isInFuture(Date())
)
But without bothering you could just:
let dates: [Date] = // ...
let futureDates = dates.filter { $0 > Date() }
// or
let timestamps: [TimeInterval] = // ...
let futureTimestamps = timestamps.filter {
$0 > Date().timeIntervalSince1970
}
Is the functional programming useless, then? Hell no!
Imagine we have a grocery product management app. Our products can have expiry dates defined, but they are not mandatory. All of our products come in within batched shipments from same distributor:
struct Product {
let id: Int
let expiryDate: Date?
}
struct Shipment {
let id: Int
let distributor: String
let products: [Product]
}
Now, when shipments start coming in we want to make sure that the distributor is not trying to sell expired products to us.
Let’s write a test case to analyse the shipment first:
class ShipmentTests: XCTestCase {
var now: Date!
var shipments: [Shipment]!
override func setUp() {
super.setUp()
now = date(at: "2018-10-15 12:43:20")
let notExpired = Product(
id: 1,
expiryDate: date(at: "2018-10-20 14:00:00")
)
let expired = Product(
id: 2,
expiryDate: date(at: "2018-08-15 19:17:23")
)
let unknown = Product(
id: 3,
expiryDate: nil
)
let anotherNotExpired = Product(
id: 4,
expiryDate: date(at: "2018-10-16 10:10:10")
)
let good = Shipment(
id: 1,
distributor: "Good",
products: [notExpired, unknown]
)
let bad = Shipment(
id: 2,
distributor: "Bad",
products: [expired, anotherNotExpired]
)
shipments = [good, bad]
}
func testThatBadShipmentsAreDetected() {
let badShipments = shipments.filter(/* ??? */)
XCTAssertEqual([2], badShipments.map { $0.id })
}
We have defined 2 separate shipments. The 2nd contains an expired product. Using the trial & error approach, we can now build a logic which determines bad shipments:
let badShipments: [Shipment] = shipments.filter { shipment in
!(shipment.products.filter { product in
product.expiryDate.map { $0 <= now } ?? false
}.isEmpty)
}
It’s a lot of transformations packed into a single statement. Although we have a test to back us up, it’s hard to visually assert whether we have all of the edge cases covered.
It’s because the code is not composed of smaller, verifiable units.
What’s worse, we have isInFuture
function defined that could help us, but we are not using it. Why? It would make the code even less readable:
let badShipments: [Shipment] = shipments.filter { shipment in
!(shipment.products.filter { product in
product.expiryDate.map { !isInFuture(now)($0) } ?? false
}.isEmpty)
}
Let’s rebuild the code into free functions for more composability and testability. We will start the analysis from the innermost bracket - !isInFuture(now)($0)
. It turns out it’s very simple to negate a function using the function composition operator:
let isNotInFuture: (Date) -> Bool = isInFuture(now) >>> (!)
!
operator is just a function with (Bool) -> Bool
signature and we are free to compose it.
Now, we’ll analyse the inner filter part in which we determine whether a product is expired or not. First, let’s build a function that can extract a value from any object given a key path:
func get<Root, Value>(_ keyPath: KeyPath<Root, Value>)
-> (Root) -> Value {
return { $0[keyPath: keyPath] }
}
Second, we’ll need to map over the optional value. We can build a free version of map
function on an optional:
func map<A, B>(_ f: @escaping (A) -> B)
-> (A?) -> B? {
return { a in a.map(f) }
}
Finally, we want to be able to convert an optional value into a non-optional value, replacing a nil
with a constant:
func some<Value>(_ defaultValue: Value)
-> (Value?) -> Value {
return { $0 ?? defaultValue }
}
Note how small and generic those components are. You could introduce them to your codebase regardless of the business domain.
Let’s now compose a function to test whether a product has expired:
func hasProductExpired(_ now: Date)
-> (Product) -> Bool {
return get(\.expiryDate)
>>> map(isInFuture(now) >>> (!))
>>> some(false)
}
The function body reads nicely. It extracts expiryDate
from a product and applies inverse of isInFuture
function to the value if it exists. Otherwise, false
is returned.
We should of course verify whether the function behaves as expected:
func testHasProductExpiredReturnsFalseForFutureExpiryDate() {
let notExpired = Product(
id: 1,
expiryDate: date(at: "2018-10-20 14:00:00")
)
XCTAssertFalse(hasProductExpired(now)(notExpired))
}
func testHasProductExpiredReturnsTrueForPastExpiryDate() {
let expired = Product(
id: 2,
expiryDate: date(at: "2018-08-15 19:17:23")
)
XCTAssertTrue(hasProductExpired(now)(expired))
}
func testHasProductExpiredReturnsTrueForNowExpiryDate() {
let expired = Product(id: 1, expiryDate: now)
XCTAssertTrue(hasProductExpired(now)(expired))
}
func testHasProductExpiredReturnsFalseForNilExpiryDate() {
let unknown = Product(id: 3, expiryDate: nil)
XCTAssertFalse(hasProductExpired(now)(unknown))
}
The tests are green for our composed function.
Before we’ll continue, let’s stop for a second and recap what has happened. Together we have:
- introduced generic, reusable functions that can be applied for any types in the system (
get
,map
,some
), - composed a brand new method called
hasProductExpired(_:Date)
, - added exhaustive tests to boost our confidence in composed function.
Now we want to know whether the shipment is bad. We could rewrite the original implementation to use our composed method:
let badShipments: [Shipment] = shipments.filter {
!($0.products.filter(hasProductExpired(now)).isEmpty)
}
It reads nicer, but we can still extract the closure applied to a filter:
func isBadShipment(_ now: Date)
-> (Shipment) -> Bool {
return get(\.products)
>>> filter(hasProductExpired(now))
>>> isEmpty
>>> (!)
}
We’ve composed yet another method! During the process, we’ve also freed up filter
and isEmpty
methods:
func filter<A>(_ f: @escaping (A) -> Bool)
-> ([A]) -> [A] {
return { a in a.filter(f) }
}
func isEmpty<A>(_ array: [A])
-> Bool {
return array.isEmpty
}
We can also refactor how bad shipments are determined:
let badShipments: [Shipment] = shipments
.filter(isBadShipment(now))
That’s a hell of refactoring. We went from:
let badShipments: [Shipment] = shipments.filter { shipment in
!(shipment.products.filter { product in
product.expiryDate.map { $0 <= Date() } ?? false
}.isEmpty)
}
to:
func hasProductExpired(_ now: Date)
-> (Product) -> Bool {
return get(\.expiryDate)
>>> map(isInFuture(now) >>> (!))
>>> some(false)
}
func isBadShipment(_ now: Date)
-> (Shipment) -> Bool {
return get(\.products)
>>> filter(hasProductExpired(now))
>>> isEmpty
>>> (!)
}
let badShipments: [Shipment] = shipments
.filter(isBadShipment(now))
reusing our isInFuture
function and also boosting our confidence in unit tests along the way.
The final step is to re-run our initial tests and prove that everything is still green after the refactoring. Hooray!
Conclusions
In this post we’ve proven that it’s possible to build a complex data transformation using free, generic & very small composable methods. Thanks to the function composition we can boost the test confidence, remove the boilerplate coming from chaining some operators[1] and drop unnamed $0
arguments referring to different data types on every nest level. Function composition also helped us reuse the already defined functions (like isInFuture
) beyond what seemed readable without it.
Credits
The post is inspired by the great content available at pointfree.co. Brandon & Stephen are doing a great job. Don't hesitate, subscribe yourself & unleash the functional beast!
See how we had to wrap the whole filter body in brackets due to the
!
operator. ↩︎