Stack Builders logo
Arrow icon Insights

Swiftz: the power of liftz

When in Rome, your functions should do as the Romans do. Automatically.

When in Rome, your functions should do as the Romans do. Automatically.

It should come as no surprise to those acquainted with Swift that the language includes support for functional idioms. Swiftz is a library that attempts to implement some of the common abstractions found in functional programming languages and makes them available to the Swift world. However, Swift is still primarily an object-oriented (OO) language so such abstractions take a slightly different form than the ones in functional languages.

The main abstraction in the functional world is the function. Hopefully it is safe to assume that everybody who might be reading this article knows what a function is. However, functions not attached to a class might seem limiting to those accustomed to the object-oriented abstractions. Specifically, those with a background in object-oriented programming (OOP) might expect to be able to use their functions within a given context implicitly. The context of the function (or method if you prefer that word) is given by the class where the function is defined.

First of all, we need to clarify what we mean with the word “context.” In the object-oriented world, context means a set of variables associated with an instance of an object. Methods defined within the same class can operate on these variables. However, the context of an instance (or class) method is also determined by the behaviour the class implements. This second variety of context is the one we talk about when we use the phrase computational context in functional programming. This might seem a little too abstract right now, but don’t despair as we will dive into some examples soon.

One of the common places where we apply a function within a different context from where the function was originally defined is when working with promises. For this example we will use the BrightFutures library for Swift. For the uninitiated, here’s an example lifted and simplified from the library’s README.md:

User.logIn(username,password)
    .onSuccess(doSomethingWithPosts)

The previous code snippet does exactly what we just described. It takes a value within a computational context and applies a function to it. In this case, the context is going to be the result of the successfully completed login action in the future. The doSomethingWithPosts function will be invoked within that context. Concretely, the .onSuccess method of the future applies the function once the Future has resolved. This method effectively allows us to apply regular functions in contexts that have yet to be resolved.

However, there are an infinite number of computational contexts where we might want to apply our functions. We have just explored one. The next example that immediately comes to mind is Swift’s built-in Optional type. As a quick recap, let us quote Apple’s documentation on them:

Optionals say either “there is a value, and it equals x” or “there isn’t a value at all."

This is yet another computational context that we can consider in the same way we considered the Future computational context. Hereafter, we will refer to values of the Optional type to represent a computational context where failure can be represented.

Luckily for us, applying regular functions within the context of an Optional has been baked into the Swift language. This is what is called Optional chaining in the language documentation. Function application in the context of possible failure (i.e. Optional) is done transparently in Swift. Let’s look at this snippet from Apple’s documentation on Optional chaining:

let roomCount = john.residence?.numberOfRooms

The residence property of the john object is an Optional. It might or might not exist. However we can look up a property on it just fine. In this example we are applying the numberOfRooms method on a value with an Optional type. Let’s take a look at Apple’s documentation again:

The fact that it is queried through an optional chain means that the call to numberOfRooms will always return an Int? instead of an Int.

This contains an interesting tidbit of information. numberOfRooms does not return an Optional, but when applied to a Residence? now it will return Int?. The compiler merely wraps the type of numberOfRooms in an Optional. This works in exactly the same way with Future’s onSuccess function as the return value of onSuccess is a Future.

Let’s stop for a second and notice the common pattern between BrightFuture’s onSuccess and Optional chaining. onSuccess will take a function and return a new Future with the function applied after the Future has resolved. Optional chaining will take a function and return a new Optional with the function applied within it if it is not nil. Let’s look at a diagram to make the pattern more evident:

Functors Diagram

Let’s try to build some more intuition before we go deeper into the functional rabbit hole. Until now we have considered two different computational contexts: the context of possible failure modeled by Optional and the context of variables that do not have a value yet modeled by Future. It’s important to note that so far we have employed only regular functions to deal with both Optional and Future. In fact we could’ve used the same function for both examples. The most interesting part to notice here is that we never take care of the context by ourselves.

Enter Swiftz

Now we will look at another interesting computational context. This is where Swiftz first comes into play. We will use Swiftz implementation of list for this example. How do we apply a regular function to a list? The answer is the map function. The relevant part can be found in Swiftz's source. In order to apply a function to a list we simply map that function over the list. The relevant code snippet:

public func map<B>(f : A -> B) -> List<B> {
    switch self.match() {
        case .Nil:
            return []
        case let .Cons(hd, tl):
            return List<B>(f(hd), tl.map(f))
    }
}

That is, we apply the function to every element in the list and return a new list with the modified values. Now, it’s important to see if this implementation fits within the framework of Optional and Future we previously built. In the same way as Optional chaining and onSuccess, map takes a value within a context (Optional, Future or List so far) and a regular function and returns a new value within the same context. We could say that optional chaining, onSuccess and map are analogous operations. How are they related? They handle the application of a plain old function within a computational context.

Optional chaining, onSuccess and map are all examples of functors in Swift. Intuitively, functors convert regular functions into functions that can operate within a given computational context. This is what we call "lifting". The functors shown so far have been slightly different than those in the functional programming world. Specifically, these Swift functors lift the function and then immediately call the lift function with a value.

Functors in Swiftz are similar to the functors we have seen so far. Swiftz defines a protocol for functors. A class that wants to conform to the functor protocol mainly needs to implement the fmap method. This allows us to call fmap on any object that conforms to the protocol. At this point we should look at the code. First, the Functor protocol from Swiftz:

public protocol Functor {
    typealias A
    typealias B
    typealias FB = K1<B>

    func fmap(f : A -> B) -> FB
}

In order to conform to this protocol a class needs to define two typealiases and one method. fmap is the common name that corresponds to both onSuccess in the case of futures and map in the case of lists. The type aliases correspond to the types of the function we are going to lift using the fmap definition. For now you can ignore the third typealias. The type signature of the fmap function is the most interesting part. fmap as defined in the protocol takes a function and produces a value within the context of the functor. But wait, isn’t something missing here? Shouldn’t fmap take a FA too? Isn’t fmap supposed to take a function and a value? Well, in the way Functors are implemented in Swiftz, fmap lifts the given function and applies it to an instance value in the class where fmap is defined. That’s where the missing FA went. Just to make it clearer that fmap and map are equivalent in Swiftz when it comes to lists, here's Swiftz's implementation of fmap for List:

extension List : Functor {
    typealias B = Any
    typealias FB = List<B>

    public func fmap<B>(f : (A -> B)) -> List<B> {
        return self.map(f)
    }
}

As you can see, fmap simply calls map.

We need to go deeper: the Basis library

Functors in the Basis library are a little closer to what functors in the functional world look like. First of all, let’s take a look at Basis’ Functor protocol:

public protocol Functor {

    typealias A
    typealias B
    typealias FB = K1<B>

    class func fmap(A -> B) -> Self -> FB
}

The primary difference here is that fmap is no longer an instance method; now it’s a class method. It follows that this fmap does not depend on the object’s state, but it needs the value passed to it explicitly. Here Self can be read as FA. It’s also important to notice that this function is curried, which means we can partially apply it. It follows that this particular definition of fmap can lift a function and then assign it to an identifier for the lifted function to be applied later.

Basis defines Maybe as its own alternative to Swift's built-in Optional. The semantics are very similar to Optional. An Optional might contain Just(Something) or it might contain Nothing. It is no longer possible to use optional chaining to work with Maybe, so a functor to the world of Maybe might prove useful. Here's how Basis defines a functor for its Maybe type:

extension Maybe : Functor {
    typealias B = Any
    typealias FB = Maybe<B>

    public static func fmap<B>(f : A -> B) -> Maybe<A> -> Maybe<B> {
        return { m in
            switch m.match() {
                case .Nothing:
                    return Maybe<B>.nothing()
                case .Just(let x):
                    return Maybe<B>.just(f(x))
            }
        }
    }
}

Let’s use this functor to apply a function to the Maybe type in Basis:

// Notice how Maybe is not even mentioned in this function definition
func sumTwo(number: Int) -> Int {
    return number + 2;
}

let liftedSumTwo = Maybe.fmap(sumTwo) // lift the function and name it liftedSumTwo. Swift infers the type as "Maybe<Int> -> Maybe<Int>"
let maybeNumber = Maybe.just(10) // define maybeNumber as a value with Maybe type

liftedSumTwo(Maybe.just(10)) // use the lifted function on the Maybe. Result:  Maybe.just(12)

Notice how the function is first lifted and then applied. No need for immediate application as in the Swiftz’s case. We can even lift it, give it a name and then use it.

So which definition is right?

There's no right or wrong definition. The Swiftz definition might be appealing to those who like OOP. The Basis definition might be more appealing to those who already program in a functional language. Personally, I like Basis' definition as it is closer to Haskell's functors, but that's just like my opinion.

note: some parts of the functor definition and the kind definition have been omitted in the interest of simplicity. The main point of this blogpost is building up intuition when it comes to functors. It is not meant to be a rigorous exposition.

Published on: May. 4, 2015
Last updated: Dec. 20, 2024

Written by:

Software Developer
Juan Pablo Santos

Subscribe to our blog

Join our community and get the latest articles, tips, and insights delivered straight to your inbox. Don’t miss it – subscribe now and be part of the conversation!
We care about your data. Check out our Privacy Policy.