During the migration of a series of projects from simple MVVM-C to the more compartmentalized CLEAN architecture, while also transitioning from Combine to the less verbose async/await, I found myself wishing for something to reduce the amount of boilerplate when mapping nested optional data.

All in all, async/await makes code more readable, which is a huge benefit for its longevity. Recently though, while refactoring a codebase from “MVVM-C + Combine” to “Clean Architecture + async/await” I ended up missing the benefits of handling data as monads in Combine, particularly when mapping models from the Data layer to the Domain layer. Wouldn’t it be amazing if we could have the best of both worlds? be able to have more readable code using async/await while preserving the utility of mapping data as a monad?

Side note: I’m not confident enough on my ability to explain what a “monad” is well enough and without repeating what other great content creators have already put out there. So here are some videos to bridge the gap on this and other functional programming concepts.

Some functional programming languages such as F# provide what is commonly known as “pipe operators”. Unfortunately, since Swift usually only emphasizes functional programming through Combine, we don’t have access to any similar operators out-of-the-box, even though some monads such as arrays do feature identical behaviours. So let’s do some plumbing and build our own! 🧑‍🔧

# Building functional pipelines

Let’s take the following two functions, which perform some arithmetical operations on integers.

func add(_ a: Int, to b: Int) -> Int {
    a + b
}

func multiply(_ a: Int, by b: Int) -> Int {
    a * b
}

If we wanted to perform a series of operations on a integers using these functions, say multiply them by two and then add 1. Let’s call this a “pipeline” of sorts 😉

let array = [1, 2, 3]
let result = array
    .map { multiply($0, by: 2) }
    .map { add(1, to: $0) }

print(result) // [3, 5, 7]

But what if we don’t have an array? What if we just want to do this operation on a single integer? Well, we could do something like this

let initialValue = 1
let intermediateValue = multiply(initialValue, by: 2)
let result = add(1, to: intermediateValue)
print(result) // 3

or this

let initialValue = 1
let result = add(1, to: multiply(initialValue, by: 2))
print(result) // 3

but both of them feel wrong in comparison with the array pipeline example 🤢. In the first example we keep everything readable by storing the intermediate value but polluting the namespace because it is unused; and in the second example we have nested the starting operation within the other one, sacrificing readability in favofr of not polluting the namespace with unused intermediate results. We can do better! 💪

The same can be achieved with Combine’s Just, which acts as a monad for a value.

let result = Just(1)
    .map { multiply($0, by: 2) }
    .map { add(1, to: $0) }

print(result) // Just<Int>(output: 3)

As you can see, it’s really useful and concise to perform functional calls on monads (f(A) -> B). We don’t sacrifice readability nor do we pollute the namespace with the intermediate results, because we just chain or “pipe” them forward 😉.

But it’s a bit impractical to wrap every entity in our codebase in a monad such as an array or Just, particularly if we are trying to move away from Combine to async/await. Queue the pipe operator |> which achieves the same behaviour without the need for such wrappers. Let’s define it.

# Pipe forward operator |>

⚠️ Note: Our pipe operators will have a precedence between ComparisonPrecedence and LogicalConjunctionPrecedence. Raising or lowering the precedence comes with tradeoffs and can result in year-long discussions (see this javascript discussion), but usually issues can be avoided by being more explicit with parenthesis. Further reading here.

precedencegroup PipeOperatorPrecedence {
    associativity: left
    higherThan: LogicalConjunctionPrecedence
}

infix operator |>: PipeOperatorPrecedence

func |><A, B>(a: A, f: @escaping (A) -> B) -> B {
    f(a)
}

It allows us to chain functional calls for values which aren’t monadic. e.g. let’s build the same pipeline from before but this time for a single integer.

let value = 2
let result = value
    |> { multiply($0, by: 2) }
    |> { add(1, to: $0) }

print(result) // 5

Yup, it’s now basically identical to the pipeline for an array of integers. We only had to replace .map with |> 🤯!

# Optional Pipe Forward operator ?|>

Going back to initial problem of mapping entities from the Data layer to the Domain layer in Clean Architecture. Imagine a function that maps data from an API to the Domain level, and that that data in turn also has underlying mappings. 🍏↔️🍎 When faced with optional values at both levels, we would have to nil-check the API level at some point before attempting to map it to the Domain level. e.g.

struct APIData {
    var apiOptionalProperty: APIPropertyType?
}

struct DomainData {
    var domainOptionalProperty: DomainPropertyType?
}

struct DataMapper {
    static func map(apiData: APIData) -> DomainData {

        var domainOptionalProperty: DomainPropertyType? = nil
        if let apiProperty = apiData.apiOptionalProperty {
            domainOptionalProperty = map(apiProperty: apiProperty)
        }

        return .init(domainOptionalProperty: domainOptionalProperty)
    }

    static func map(apiProperty: APIPropertyType) -> DomainPropertyType {
        // Some underlying mapping ...
    }
}

This is fine for a single optional mapping but, since we have to repeat the same nil-checking dance for every optional property that is to be mapped, this can quickly add unnecessary verbosity and boilerplate to the mapping functions. We could create mapping functions that take optional arguments, but then we would just be kicking this can further down 🥾🥫 Instead, let’s provide a ?|> piping operator that deals with the nil-checking for us, and we’ll even make use of the |> pipe forward operator we defined earlier 🧠

infix operator ?|>: PipeOperatorPrecedence

func ?|><A, B>(a: A?, f: @escaping (A) -> B) -> B? {
    guard let a else { return nil }
    return a |> f
}

Now we can reduce the above to a more readable functional format, where we can chain the mapping if the value passes the nil check 🪄

struct APIData {
    var apiOptionalProperty: APIPropertyType?
}

struct DomainData {
    var domainOptionalProperty: DomainPropertyType?
}

struct DataMapper {
    static func map(apiValue: APIData) -> DomainData {
        .init(
            domainOptionalProperty: apiValue.apiOptionalProperty ?|> { map(apiProperty: $0) }
        )
    }
}

but since this is also declared as an infix operator, as long as we are consuming the same number of arguments as the ones being piped, we can even shorten it a little bit further by discarding the explicit closure wrapping 🤏

domainOptionalProperty: apiValue.apiOptionalProperty ?|> { map(apiProperty: $0) }
// can also be expressed as
domainOptionalProperty: apiValue.apiOptionalProperty ?|> map(apiProperty:)

Consider the use of ?|> as “optional chaining” for functions outside of a property’s scope 🧠

apiValue.apiOptionalProperty ?|> map(apiProperty:)
// vs
apiValue.apiOptionalProperty?.map(apiProperty:) // ⚠️ invalid code, just an example of similarities

# What about Optional.map?

After posting this entry online, a kind reader on reddit (thanks Maury_poopins, you’re amazing!) quickly pointed out that Swift already offers the same behaviour as the optional Pipe Forward operator ?|> out-of-the-box. I never knew about it, and chances are you didn’t as well, so here’s the documentation for it. Optional now acts more like a classic monad, and taking the last comparison snippet, let’s see it in action

apiValue.apiOptionalProperty ?|> map(apiProperty:) // using optional pipe forward
// vs
apiValue.apiOptionalProperty.map(Self.map(apiProperty:)) // using built-in `Optional.map`
// vs
apiValue.apiOptionalProperty?.map(apiProperty:) // ⚠️ still invalid, but "ideal" code

Now, in this scenario where we are using a static mapper function, I’d argue that the ?|> is still more readable, as we can ommit the pesky type reference (Self) to chain the other mapping function. However, since adding a new operator, especially for something that can already be done out-of-the-box, might be considered overengineering and adds another thing to be maintained, I’ll leave it up to you whether it is worth it or not.

Nonetheless, I will also entice you to ponder keeping both around, as I do think that chaining methods does become easier to read when combining both of these concepts, but I’ll let you be the judge. Let’s introduce another basic function that can return nil to our previous examples and chain it ⛓️

func divide(_ a: Int, by b: Int) -> Int? {
    guard b != 0 else { return nil }
    return a / b
}

let result = 5
    |> { multiply($0, by: 10) }
    |> { divide($0, by: 2) }
    ?|> { add(1, to: $0) }
print(result) // Optional(26)

// vs

let result = 5
    |> { multiply($0, by: 10) }
    |> { divide($0, by: 2) }
    .map { add(1, to: $0) } // ❌ error: value of type '(Int) -> Int?' has no member 'map'
print(result)

In fact, as you can see, Optional.map doesn’t even lend itself to the chaining of operations with |> 😞, at least not without doing some more plumbing of our own to interface between the two (a problem brought on entirely by ourselves, but that is neither here nor there 🙈🙊).

# Other operators?

This has quickly become one of my favorite tools in my Swift developer toolbox 🧰 since first discovering it through PointFree’s brilliant inaugural video on the topic.

As always, custom operators can be both a blessing and a curse, especially if not properly documented, so create them sparingly, and if possible stick as close as possible to well documented official sources you might be taking inspiration from ⚠️.

Let me know if you find or create other useful operators, I’m always looking for ways to simplify and improve the codebases I work on 🧑‍🔧. Until next time 👋




# Sources