Monads

A practical example

Sergio Ordine
6 min readJul 25, 2018

To conclude my monad series of posts, here is a practical example that I hope helps to make thinks clearer. If you have not seen this post you can check it here and here, or start by a specific post here.

The basic examples of monads in Swift are usually Arrays and Optionals. I thought about using a different one: a way to isolate the boilerplate code we use for asynchronous calls in Swift, that I check after its development that are very close to the concept of Promises

A promise represents an eventual completion, or error, of an asynchronous operation. It represents exactly a promise of a value that does not exists or that has not been computed yet.

Rationale

The concepts used to implement it started thinking about the way we generally start an asynchronous call in Swift and how we handle it :

I just eliminated the error handling part of the code to make it more simple and focus on the idea, not the implementation.

We can think on a asynchronous call as a boilerplate code that starts another thread (using in this case the DispatchQueue class), execute a lonk term task (doSomething) that returns a value. This value is delivered to the caller though a callback function (completionHandler). We can think on a generic type associated to a type T to handle this structure, with two functions: doSomething() → T and completionHandler(T). Let's call this type Promise<T>, a promise to the calling code that a value of type T will be delivered to it, eventually, in the future.

This concept could be expanded to get the asynchronous return value and transform to something else before returning it. It could be used to reuse this code in a similar, but different context:

We could think on a way to expand our generic type that would execute things asynchronously to transform a value and return a different type U. It is a strong candidate to have a map function map(T → U) → Promise<U>. We can think on making our Promise not just a generic type, but also a Functor!

More than that, we can execute an asynchronous code and, based on this result start another asynchronous call:

This code executes a long duration task on doSomethingAsynchronously, that returns a value o type T. Based on this value, the function transformToAsynchronous returns a new task function that can be executed on another asynchronous context (I used global on both just for simplicity). We can think on this function as something that converts T to a function with no parameters that returns something of type U: () → U. We could consider embedding this function into a Promise, as we did with doSomethingAsynchronously in the previous examples. So we can think on a function T → Promise<U> and a function that make this second Promise run as a result from the first one. This is a strong candidate for a function flatMap(T → Promise<U>) → Promise<U>. The Promise can be implemented as a Monad in this case!

Implementation

The first step is creating a class that holds the asynchronous call, the base code for Promise<T>:

The doSomething function was renamed to task, the long duration computation that will be executed asynchronously. It is a function with no parameters that returns a value of type T. The onCompletion function defines what should be executed when this computation finishes (the completionHandler function that receives this value T, and uses its).

The onCompletion is responsible for start the new thread, using the DispatchQueue.global queue, execute the task and returns its value to the completionHandler logic.

There are two initializers: one that receives the task to be executed and keeps it, and another that gets a value and just creates a task that returns it. This second initializer can be seen as "puting the value into another thread" and was implemented to represent the monad a → M(a) , the monoid "identity/neutral" element.

The next step is creating the map function, to make Promise a functor. We must remember that on the Promise context, the value is not yet presented when we call the map function, so we need to postpone the transformation to the moment when the onCompletion function is called. In a similar way that RxSwift works, let’s create another object Promise to keep the original task and transform it when the value is received:

This PromiseMap is a class inherited from Promise<Destination> type that holds the original promise and the transformation to be performed:

The override function onCompletion of this promise calls the original one and passes to that original promise a completionHandler that transforms its returned value into a new one (of Destination type), using the transform function and then, calls the completionHandler.

As the map function returns a new Promise object, we can think as the original promise onCompletion is not exposed anymore to the calling code (just the onCompletion of the mapped one):

That is why we can "hijack" the original promise onCompletion and create an internal completionHandler as we did. It fires the original task into a new thread and, in that same thread transform the object and returns it to the exposed completionHandler(of the second promise)!

Diagram of PromiseMap

Finally, we need to implement the flat map method, that will chain two distinct Promises:

This function transform the original promise into a promise of type Promise<Promise<Destination>>, using the map function. We need to "flat" it in order to make it just a Promise<Destination>. In a similar way we did with map we also need to postpone this, because the inner Promise<Destination> will be available just after the original promise executes and the transformation is performed. Flat in this case means wait the execution of the first promise, create a new Promise<Destination> from its result and call asynchronously it. That flattening will be performed by the NestedPromise class:

This class keeps the nested promises when flatMap is called over the mapped one (Promise<Promise<T>>). When its onCompletion is called, it calls the mapped promise onCompletion (it hijacks it, in the same way the PromiseMap did with the original Promise). The completion handler receive the Promise<T> created from the original result. We can then pass the completionHandler the external code send to NestedPromise to this "inner" promise as both handles a value of type T!

Diagram of NestedPromise

Using Promise

The following sample present the usage of Promise and its map and flatMap functions:

We start by a Promise (nasaURL) that just put a string, representing and URL of a REST API, into a asynchronous thread. This API returns information about a astronomy related photo from NASA that changes on a daily basis.

The nasaURL promise calls it map function, passing the imageURL function. The imageURL function accesses the API, parses the returned JSON and gets an specific data within it (hdurl field) that represents another URL, from a high definition image itself.

This image is then recovered asynchronously by the promise created by downloadImage function. This function is chained to the previous one using the flatMap. The onCompletion block that follow is executed after all this steps: recovering the JSON, parsing it and retrieving the image.

Notice that all code that handles the asynchronous calls is hidden on this code, as it is all handled by the promises. So, this technicality is not presented on this code, making the application logic (that is: retrieve the image) more evident.

We could even create custom operators to make the chain more explicit:

Using those operators, the application logic could be represented as:

This tutorial is an example on how we can isolate boilerplate code using generics, and its specialization as functors and monads for chaining functions hiding some of the computational and technological complexity not directly related to the logic we are implementing.

PS: This is a very basic implementation of a Promise, made just for exemplify a monad different from the usual ones in Swift. It does not handle errors, allows choosing the queue or thread where the task is performed (and we need at least to run it on the main thread for UI tasks) between other limitations. Those features should be implemented in order to make it useful in a real world project.

--

--

Sergio Ordine

Software developer and educator. If you want to support me and my content production, please buy me a beer at https://www.buymeacoffee.com/sergioordine