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.