Result Oriented Programming: building a result incrementally using an applicative functor

Result Oriented Programming: building a result incrementally using an applicative functor

In my previous story, we looked at how we could build a Result incrementally using function composition. We built three variations of the function applyResult. Composing applyResult with flatMap allowed us to take the following piece of code:

func getTweetDetails(for userId: String) 
-> Result<TweetDetails, EmojiErr> {
    var user: User!
    var tweet: Tweet!
    
    return getUser(with: userId)
        .flatMap { userData -> Result<Tweet, EmojiErr> in
            user = userData
            return getLatestTweet(for: userData)
        }.flatMap { 
           tweetData -> Result<TweetSentiment, EmojiErr> in
           tweet = tweetData
           return getTweetSentiment(for: tweetData)
        }.flatMap { sentimentData in
           .success(TweetDetails(user, tweet, sentimentData))
    }
}

and improve it in the following way:

func getTweetDetails(for userId: String) -> Result<TweetDetails, EmojiError> {
    let tweetDetails = curry(TweetDetails.init)
    return applyResult(of: getUser(), to: tweetDetails)
        .flatMap(applyResult(of: getLatestTweet))
        .flatMap(applyResult(of: getTweetSentiment))
}

A good friend and an exceptional engineer recently watched a video by Stephen Celis called Applicatives and Swift. After reading my previous article, he suggested I watch the video. By using an applicative functor, he believed, the solution could be simplified even further.

I must admit, I had to watch the video a few times before fully grasping the concept. It is an excellent video and one I would highly recommend, even if for only watching Stephen Celis live code the entire thing.

After experimenting in Playgrounds, I was able to simplify the above into something quite elegant:

func getTweetDetails(for userId: String) -> Result<TweetDetails, EmojiError> {
    return pure(createTweetDetails) 
        <*> getUser() 
        <*> pure(getLatestTweet) 
        <*> pure(getTweetSentiment)
}

This post will detail just how I was able to achieve the above using an applicative functor.

Where does the applicative functor fit in?

Before we begin, let’s first discuss applicatives. If we look at the signature of map(functor), and flatmap(monad) implemented in the Result Type, we have the following:

((A -> B),  Result<A, Error>) -> Result<B, Error>
((A -> Result<B, Error>), Result<A, Error>) -> Result<B, Error>

I am not going to discuss functors or monads, because this article does a tremendous job doing just that. But an applicative functor just extends a functor by now wrapping our functions in context as well. This leaves us with the following signature for an applicative functor:

(Result<(A -> B), Error>, Result<A, Error>) -> Result<B, Error>

At first glance that might not seem the most useful, but let us first implement the applicative then use it to show its usefulness.

Building the Applicative Functor

We will start by first creating our operator:

precedencegroup LeftAssociativity {
    associativity: left
}

infix operator <*>: LeftAssociativity

Now let us create a simple function called pure. This function will take a value and wrap it in a Result.

func pure<A>(_ x: A) -> Result<A, EmojiError> {
    return .success(x)
}

And now for the actual applicative implementation:

func <*> <A, B, E>(f: Result<(A) -> B, E>, x: Result<A, E>) -> Result<B, E> {
    return f.flatMap(x.map)
}

Let us take the above for a test run by creating the following struct:

struct Tweet {
    let id: Int?
    let tweet: String
}

Now let us create a simple curried function that builds a Tweet incrementally:

let createTweet = { id in { tweet in return Tweet(id: id, tweet: tweet) } }

Last step is to create two simple validating functions for our inputs:

func validateId(_ id: Int) -> Result<Int, EmojiError> {
    return id > 0 
        ? pure(id) 
        : .failure(.😩("invalid id"))
}

func validateTweet(_ tweet: String) -> Result<String, EmojiError> {
    return tweet.characters.count < 140 
        ? pure(tweet) 
        : .failure(.😩("tweet too long"))
}

Finally we end up with the following functionality:

let tweet = pure(createTweet) 
    <*> validateId(1) 
    <*> validateTweet("Hello Twitter")

Awesome.

Try passing in an invalid id or a tweet that is longer than 140 characters. We get a failable initializer for free. Hopefully that gives you some idea of why an applicative can be so useful.

Back to the “real-world” example

If you would like to follow with a working solution, please view the following playground file.

Let us look at the following piece of code and determine how we are going to fufill the criteria:

let createTweetDetails = {
    user in {
       tweet in {
          sentiment in
          TweetDetails(
               user: user, 
               tweet: tweet, 
               sentiment: sentiment
          )
       }
    }
}

So we need to first execute a function by fetching the current user. We would then use the the user’s data* to get their latest tweet*. Finally we would use the tweet’s data* to get the *tweet’s sentiment. We do this while still preserving the Railway Oriented Programming pattern. If any of the previous functions were to fail, the error falls through and the following functions will not be executed.

So unfortunately if we have the following function signatures:

func getUser() -> Result<User, EmojiError>
func getLatestTweet(for user: User?) -> Result<Tweet, EmojiError>
func getTweetSentiment(for tweet: Tweet?) -> Result<TweetSentiment, EmojiError>

Our previous applicative implementation requires the following:

let user: Result<TweetDetails, EmojiError> =
    pure(createTweetDetails)
    <*> getUser()
    <*> getLatestTweet(for: User)
    <*> getTweetSentiment(for: Tweet)

Changing the function signatures is not an option, so just like we created different variations of applyResult we can create different variations of our applicatives. Stay with me because this can get a little confusing at first.

So what do we need to do?

Let’s break it down:

  • We need to initially wrap our creating function in a Result.
  • We then execute a function which returns us a Result.
  • Our applicative will then unwrap the Result to determine if it is of type .success or .failure. If it is the latter, we do nothing and the error is passed through. If it is a successful Result, we take the unwrapped value and pass it into our creating function building it incrementally.
  • We finally take the unwrapped value and pass it into the next function doing the previous steps again until we have a completed model.
  • Remember if at any stage we get a .failure the following functions will not be exectuted, and we get passed the point of failure.

Whoa. That sounds like a lot. But it’s just like putting a puzzle together. Because Swift is a static language we have the compiler to help us. We simply cannot put the wrong puzzle pieces together.

Carrying on. Let’s look at the current implementation of our applicative:

func <*><A, B>(f: Result<(A) -> B, EmojiError>, x: Result<A, EmojiError>) -> Result<B, EmojiError> {
    switch (f, x) {
    case let (.success(f), _): return x.map(f)
    case let (.failure(e), .success): return .failure(e)
    case let (.failure(_), .failure(e)): return .failure(e)
    }
}

Now look at the following piece of code:

let result: Result<(Tweet) -> (TweetSentiment) -> TweetDetails, EmojiError> = pure(createTweetDetails) <*> repo.getUser()

Our createTweetDetails requires a Tweet. Executing getLatestTweet() will return back a Tweet. Unfortunately, getLatestTweet, which is the next function we need to exectute, requires a User as well. We can achieve this by taking a tuple and wrapping it in a Result:

let result: Result<(
(Tweet) -> (TweetSentiment) -> TweetDetails, User), EmojiError> = pure(createTweetDetails) <*> repo.getUser()

We can then pass the previous functions returned value into the next function we want to execute. We do this with another implementation of our applicative:

func <*><A, B>(f: Result<(A) -> B, EmojiError>, x: Result<A, EmojiError>) -> Result<(B, A), EmojiError> {
    switch (f, x) {
    case let (.success(r1), .success(r2)): 
        return x.map { v in (r1(v), r2) }
    case let (.failure(e), .success): 
        return .failure(e)
    case let (_, .failure(e)): 
        return .failure(e)
    }
}

We finally need to create another implentation for our applicative that will accept a function that returns a result. The applicative will unwrap the tuple and pass the second value into the function. Finally unwrapping the result of the function and passing it into createTweetDetails.

func <*><A, B, C>(f: Result<((B) -> C, A), EmojiError>, x:  Result<(A) -> Result<B, EmojiError>, EmojiError>) -> Result<(C, B), EmojiError> {
    switch (f, x) {
    case let (.success(r1), .success(r2)): 
        return r2(r1.1).map { v in (r1.0(v), v) }
    case let (.failure(e), .success):
        return .failure(e)
    case let (_, .failure(e)): 
        return .failure(e)
    }
}

We want to return a Result without the previous value passing into the next function, because our TweetDetails model would be complete. This is simply the above without returning a tuple:

func <*><A, B, C>(f: Result<((B) -> C, A), EmojiError>, x:  Result<(A) -> Result<B, EmojiError>, EmojiError>) -> Result<(C, B), EmojiError> {
    switch (f, x) {
    case let (.success(r1), .success(r2)): 
        return r2(r1.1).map { v in r1.0(v) }
    case let (.failure(e), .success):
        return .failure(e)
    case let (_, .failure(e)): 
        return .failure(e)
    }
}

Finally we end up with:

func getTweetDetails(for userId: String) -> Result<TweetDetails, EmojiError> {
    return pure(createTweetDetails) 
        <*> getUser() 
        <*> pure(getLatestTweet) 
        <*> pure(getTweetSentiment)
}

Conclusion

I know the above can be quite a lot to wrap your head around. But it gets easier and easier. The above example may seem a little trivial, but it perfectly demonstrates how we can obey the Railway Oriented Programming pattern while using an applicative. Stephen Celis was kind enough to give my article a read. He pointed out the following:

The main thing I eventually learned was that applicatives shine when the values
sent to the function are all independent. In your example, we need the result of the user request before we can make the tweet requests, so we can’t run them in parallel.

In other words an applicative is not the best choice for the above implementation. An applicative shines when you accumulate errors. Monads have restrictions that applicatives don’t: namely they’re railroad-oriented and when a monad switches tracks to the error case, they’re done.

The experimentation is fun though and it helps to have the veterans help you along the way.

You can find all the working code in the following repository. It will be easier to follow than the snippets above.

Show Comments