EP 5 · Standalone · Feb 26, 2018 ·Members

Video #5: Higher-Order Functions

smart_display

Loading stream…

Video #5: Higher-Order Functions

Episode: Video #5 Date: Feb 26, 2018 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep5-higher-order-functions

Episode thumbnail

Description

Most of the time we interact with code we did not write, and it doesn’t always play nicely with the types of compositions we have developed in previous episodes. We explore how higher-order functions can help unlock even more composability in our everyday code.

Video

Cloudflare Stream video ID: 8643cbfc7c4ef05086c83a9e2363191e Local file: video_5_higher-order-functions.mp4 *(download with --video 5)*

References

Transcript

0:00

Today we’re going to dive a bit deeper into function composition. While function composition can seem simple, it’s not always so easy to take the existing functions we work with every day and fit them into our compositions. Let’s explore some reusable techniques that can aid us in transforming functions that are difficult to compose into pieces that fit snugly. Curry

0:42

In our episode on side effects, we controlled the side effect of a function by moving a dependency on the mutable state of the world into the function’s inputs. func greet(at date: Date, name: String) -> String { let seconds = Int(date.timeIntervalSince1970) % 60 return "Hello \(name)! It's \(seconds) seconds past the minute." }

0:55

We can’t compose other functions into this function because it takes two inputs, but we found that we can fix this with a trick by taking our dependency on Date up front and immediately returning a brand new function that composes. func greet(at date: Date) -> (String) -> String { return { name in let seconds = Int(date.timeIntervalSince1970) % 60 return "Hello \(name)! It's \(seconds) seconds past the minute." } }

1:35

We fixed this in a very ad hoc way, but we can generalize the process, taking in existing functions that take multiple arguments and producing, instead, a nested chain of functions that take a single argument. This is called currying. func curry<A, B, C>(_ f: @escaping (A, B) -> C) -> (A) -> (B) -> C { return { a in { b in f(a, b) } } } Now we have an all-purpose function that, given a function that goes from two arguments, A and B , to C , returns a function that takes a single argument in A and returns a brand new function from B to C .

2:25

Let’s take a look at our greet function. greet(at:name:) // (Date, String) -> String

2:32

Swift allows us to refer to a function or method by specifying the arguments but omitting the data.

2:45

What happens when we feed it our original greet function? curry(greet(at:name:)) // (Date) -> (String) -> String

2:48

This looks just like our manually-curried version. greet(at:) // (Date) -> (String) -> String

3:11

If you’re wondering why it’s called “curry”, it’s named after the logician and mathematician, Haskell Curry, for which the programming language Haskell is also named. While he popularized the idea of currying, it was discovered by Moses Schönfinkel. Some folks wanted to call this process “Schönfinkelization,” but luckily we don’t have to regularly puzzle through that spelling.

3:44

There’s a whole world of multi-argument functions that we work with every day in the Swift standard library, core frameworks, and third-party code. With currying, we can now use these functions and fit them into our compositions.

3:55

For example, we have a String initializer that takes both Data and an Encoding : String.init(data:encoding:) // (Data, String.Encoding) -> String? Let’s curry it. curry(String.init(data:encoding:)) // (Data) -> (String.Encoding) -> String?

4:08

Now we have a brand new function that may compose better into our pipelines. Flip

4:15

When we curry these functions and try to compose with them, we start to see a pattern. Multi-argument functions often seem to order their arguments a very specific way: they take the important, at-hand data as their first arguments, and any configuration options after.

4:46

In this String initializer, we have a function that takes data first, and then an encoding. When we curry it, it still doesn’t seem to compose with our programs. The most relevant data at runtime is the Data as input, and the optional String as output, but what we have here is a function that takes Data as input and returns a new function, (String.Encoding) -> String? as output.

5:01

We could compose into a new function that handles this configuration. curry(String.init(data:encoding:)) // (Data) -> (String.Encoding) -> String? >>> { $0(.utf8) } // (Data) -> String?

5:18

This is a bit awkward, especially when we consider that we’d have to sneak this configuration in wherever we use these functions. We want reusability without repetition. In this case the encoding is often fixed configuration, and usually just UTF-8 at that, so it makes sense to diminish or hide this detail from the caller. One way of doing that is taking the configuration up front, but currying seems to flip the argument order we care about when calling our functions.

5:46

We should be able to generalize the process of flipping a function’s arguments around. func flip<A, B, C>(_ f: @escaping (A) -> (B) -> C) -> (B) -> (A) -> C { return { b in { a in f(a)(b) } } }

6:15

Let’s try using it with the String initializer. flip(curry(String.init(data:encoding:))) // (String.Encoding) -> (Data) -> String?

6:26

We can now store this in a helper. let stringWithEncoding = flip(curry(String.init(data:encoding:)))

6:30

And we can derive another helper for our most common case. let utf8String = stringWithEncoding(.utf8) // (Data) -> String?

6:43

And now we have something that composes much better. Data is something that’s scattered throughout our application: we get it from APIs, we read it from disk. Meanwhile, UTF-8 encoding is something we can push to the boundary since we only need to configure this a single time for each call.

7:08

So far we’ve only built these tools to handle functions with 2 arguments, but we can write any number of curry overloads that take functions of 3 arguments, 4, or more. Our flip function is also generalized to flip just the first two arguments of a curried function. Things may seem trickier if we want to flip the order of 3 or more arguments. We’ll have to save that for a future episode. Unbound methods

7:18

With these generalized flip and curry functions in our toolchain, we can now easily massage composition out of functions that may not have composed out of the box. But there’s still a ton of code out there that seems out of reach: methods.

7:40

Swift does something nice here: it generates static functions for every instance method on a type. This is nice because we can refer to this functionality in the abstract without having our data at hand. Let’s look at an example method call: "Hello".uppercased(with: Locale(identifier: "en")) // "HELLO"

8:06

We can reference this method on the type itself: String.uppercased(with:) // (String) -> (Locale?) -> String

8:17

Huh! What’s going on here? We have a curried function from String to Locale? to String again. In general, these static members have a signature: // (Self) -> (Arguments) -> ReturnType In this case, Self is String and uppercased(with:) takes an optional Locale as input. We now have access to method logic without needing the data! Let’s try using it. String.uppercased(with:)("Hello") // (Locale?) -> String

8:52

Now we need to plug in the locale. String.uppercased(with:)("Hello")(Locale.init(identifier: "en")) // "HELLO" It worked, but we have our ordering problem all over again! We’re forced to provide the data up front, while the configuration, the Locale , is deferred. Luckily, we have flip ! flip(String.uppercased(with:)) // (Locale?) -> (String) -> String

9:15

This is looking nice. We’ve reordered to prioritize configuration so that, given a Locale , we have a reusable function that takes data.

9:30

We can now create helpers from this base function. let uppercasedWithLocale = flip(String.uppercased(with:))

9:37

We can further create more specific helpers. let uppercasedWithEn = uppercasedWithLocale(Locale(identifier: "en"))

9:51

And we can finally pipe in some data. "Hello" |> uppercasedWithEn // "HELLO"

9:57

Pretty amazing! It’s a simple case, but we’ve seen that these kinds of functions compose so easily! We work with a lot of existing code every day, so it’s nice to see that when these functions and methods don’t compose out of the box, we can use just a couple small tools to fix the problem! It seems like we’ve figured it all out! A problem

10:18

Not quite. Let’s take a look at another example. There’s another uppercasing method on String . String.uppercased // (String) -> () -> String

10:46

This function signature looks a bit funny, but we can kind of make sense of it. It has that empty argument list in the middle instead of the locale we saw earlier.

10:58

If we think back to our static, curried methods, our Self is String , it’s a method that needs to be called, but it’s called without any arguments, which is our empty () , before returning a String .

11:09

Let’s try flipping it: flip(String.uppercased) // (Locale?) -> (String) -> String

11:17

What happened? Where’d that Locale? come from? This method is overloaded in Swift, and without specifying arguments, it’s preferred the version that will compile with our flip function. We’ve defined flip in a way that doesn’t work with zero argument methods. This should be easy to fix! Let’s define an overload. func flip<A, C>( _ f: @escaping (A) -> () -> C ) -> () -> (A) -> C { return { { a in f(a)() } } }

12:00

It looks a little funny, but our function now flips as we expect. flip(String.uppercased) // () -> (String) -> String

12:07

So how can we use this function? Well, in its current state it takes zero arguments, so let’s call it. flip(String.uppercased)() // (String) -> String This is looking weird but better, we can finally send our data through. "Hello" |> flip(String.uppercased)() // "HELLO" Let’s take another look at this. flip(String.uppercased) // () -> (String) -> String flip(String.uppercased)() // (String) -> String Those parentheses. It looks like we have another parentheses problem. Zurry

12:42

So, what can we do with such functions? Pretty much the only thing we can do is evaluate them and get a value of type A out of it. I have a colleague, Eitan Chatav , from back when I was in grad school that has a wonderful name for this operation: zurry ! You can think of it as a zero-argument curry: func zurry<A>(_ f: () -> A) -> A { return f() } It’s just a lil helper function that lowers a function that takes no arguments to just the value that it returns.

13:20

Now we can take our flipped uppercased function and zurry it. zurry(flip(String.uppercased)) // (String) -> String

13:27

And we can pipe data through. "Hello" |> zurry(flip(String.uppercased)) // "HELLO"

13:35

Zurry is kind of nice! It has a cute name, it’s a function with a simple task, and it helped with our parentheses problem. We could take it or leave it by calling our flipped, zero-argument functions directly, but zurry is still a fun concept to consider whenever we deal with this problem. Higher-order

14:32

We’ve gotten a lot of leverage out of curry , flip , and zurry ! What’s interesting is that they’re all functions that take functions as input and produce functions as output. A lot of our composition functions do the same! These are called “higher order functions”, and it’s interesting to see how they work with composition in general.

15:19

Let’s explore another example of a method that we use a lot but maybe doesn’t compose as well in the method world: [1, 2, 3] .map(incr) .map(square) In the method world, composition is hard to see, so let’s bring it into our free function world. Can we use our new tools? If we start with curry , what does that look like? curry(Array.map) Cannot convert value of type ‘ (Array<_>) -> ((_) throws -> _) throws -> [_] ’ to expected argument type ‘ (_, _) -> _ ’ We have a problem. Array is generic. I guess we know we’re working with Int s here, so we could constrain it. curry([Int].map) Cannot convert value of type ‘ ([Int]) -> ((Int) throws -> _) throws -> [_] ’ to expected argument type ‘ (_, _) -> _ ’ The map method is also generic with its return value, so it looks like we need to specify the whole thing. curry([Int].map as ([Int]) -> ((Int) -> Int) -> [Int]) Cannot convert value of type ‘ ([Int]) -> ((Int) throws -> _) throws -> [_] ’ to type ‘ ([Int]) -> ((Int) -> Int) -> [Int] ’ in coercion

16:42

It’s still not happy! There are all these throws that we’re not accounting for.

16:47

This isn’t looking good: curry (and probably flip ) seem to become less useful in complicated situations where generics and throws are involved.

17:06

Let’s avoid these issues and write a free function version of map by hand. We can define it in our own terms! Let’s flip and curry things by taking our configurable transform function first, and returning a new function from our array of data to a new array. func map<A, B>(_ f: @escaping (A) -> B) -> ([A]) -> [B] { return { $0.map(f) } }

17:49

Let’s try it out. map(incr) // ([Int]) -> [Int] That’s neat. We have a brand new function from [Int] to [Int] . map(square) // ([Int]) -> [Int] Another reusable function.

18:01

These compose! map(incr) >>> map(square) // ([Int]) -> [Int] By freeing map it becomes reusable in a really interesting way. We can even change the type. map(incr) >>> map(square) >>> map(String.init) // ([Int]) -> [String] We’ve built another pipeline without data!

18:33

We’ve also discussed this before: composing map s is the same as composing the functions passed to map s a single time. map(incr >>> square >>> String.init) // ([Int]) -> [String] This is also more efficient.

18:55

There are other higher-order functions that compose, though. Like filter . Array(1...10) .filter { $0 > 5 } // [6, 7, 8, 9, 10]

19:21

Let’s define a free filter function. func filter<A>(_ p: @escaping (A) -> Bool) -> ([A]) -> [A] { return { $0.filter(p) } }

19:48

Because we’ve curried this function and following our rule about argument ordering, we can build lightweight, reusable filtering functions very easily. filter { $0 > 5 } // ([Int]) -> [Int]

19:55

Further, these filtering functions compose into our map functions. filter { $0 > 5 } >>> map(incr) // ([Int]) -> [Int]

20:03

And we can further compose in ways our intuition guides us. filter { $0 > 5 } >>> map(incr >>> square) // ([Int]) -> [Int]

20:07

We’ve built a nice bit of functionality without even having data! We can describe our program logic in an abstract way before we ever use it. Let’s see it in action: Array(1...10) |> filter { $0 > 5 } >>> map(incr >>> square) // [49, 64, 81, 100, 121]

20:30

There are a ton of other higher-order functions out there that are just waiting to be curried, flipped, and composed together. What’s the point?

20:39

It’s time that we ask ourselves, “What’s the point?” We’ve introduced curry , zurry , and flip as reusable machinery to transform a function’s shape and unlock composition. It’s nice when it works, but we quickly hit some limitations. Is it worth introducing these functions to our code bases?

21:13

We think so! When they work, they work really well, saving us a lot of boilerplate. When they don’t work, we’re still using the concepts and building intuitions for them. The ideas of currying and flipping are simple, and they’re going to unlock a lot of composition in the future.

22:15

We’re going to use these tools a lot in episodes to come in very powerful ways. Stay tuned! References Everything’s a Function. Eitan Chatav • Jan 18, 2013 This short article explains how everything can be seen to be a function, even values and function application. Eitan coins the term zurry to describe the act of currying a zero-argument function. https://tangledw3b.wordpress.com/2013/01/18/cartesian-closed-categories/ Downloads Sample code 0005-higher-order-functions Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .