A difference in perspective

Recently, Erica Sadun wrote about inconsistencies in Swift’s function-like constructs, such as KeyPath and method references—a post that appears lost in the apocalypse befalling her site.

Erica pointed out that method references return (A) -> () -> B functions that aren’t directly useable by higher order functions like map, which takes an (A) -> B function.

// Basic example
["a", "b"].map { "a".uppercased() } // ["A", "B"]

// Using a method reference. Hmm. Not quite what we wanted
["a", "b"].map(String.uppercased) // [() -> String, () -> String]

// Junky alternatives
["a", "b"].map(String.uppercased).map { $0() }
["a", "b"].map { String.uppercased($0)() }

She then works through a couple of solutions. A custom map style function on Sequence taking such a function and hiding the extra function application, and eventually an apply function that converts (A) -> () -> B to (A) -> B (and an accompanying operator).

One thing that struct me was how Erica derived all this from first principles, essentially reimplementing a function called flip, a common function in functional programming circles. From my perspective I immediately saw the problem as:

Oh, I need to flip this function and partially apply it to ()

Unfortunately, because we’re using Swift, it’s not quite that easy.

func flip<A, B, C>(f: (A) -> (B) -> C) -> (B) -> (A) -> C {
    return { b in { a in f(a)(b) } }
}

// Doesn't compile! For a couple of Swiftastic reasons.
["a", "b"].map(flip(String.uppercased)())

Swift treats () parameters as a special case, not just another type, so () can not be used as a B. Let’s write a special case version:

func flip<A, B>(_ f: @escaping (A) -> () -> B) -> () -> (A) -> B {
    return { { a in f(a)() } }
}

// This one does compile! Though you may notice it looks to have some redundancy.
["a", "b"].map(flip(String.uppercased)())

And we can now reimplement Erica’s apply in terms of our special case of flip:

func flap<A, B>(_ f: @escaping (A) -> () -> B) -> (A) -> B {
    return flip(f)()
}

["a", "b"].map(flap(String.uppercased))

Or just remove the intermediary special case of flip altogether, arriving at exactly Erica’s solution (operator not withstanding):

func flap<A, B>(_ f: @escaping (A) -> () -> B) -> (A) -> B {
    return { a in f(a)() }
}

["a", "b"].map(flap(String.uppercased))

In the end, we’ve arrived at the same solution to this specific problem. However, I hope I’ve illustrated how viewing programming from a slightly different perspective let us identify the issue quickly, and build a solution out of existing smaller, composeable pieces.

Ryan Booker @ryanbooker