EP 11 · Standalone · Apr 9, 2018 ·Members

Video #11: Composition without Operators

smart_display

Loading stream…

Video #11: Composition without Operators

Episode: Video #11 Date: Apr 9, 2018 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep11-composition-without-operators

Episode thumbnail

Description

While we unabashedly promote custom operators in this series, we understand that not every codebase can adopt them. Composition is too important to miss out on due to operators, so we want to explore some alternatives to unlock these benefits.

Video

Cloudflare Stream video ID: e9be02b24d9ea8db801d5da067777757 Local file: video_11_composition-without-operators.mp4 *(download with --video 11)*

References

Transcript

0:05

Our series unabashedly promotes the use of custom operators, but they’re far from common in the Swift community. Our use of operators may even be the most controversial aspect of our series! We explored the “why” of operators in our first episode, and we continue to justify the operators we introduce with specific—maybe even rigorous—criteria, and while we may have you convinced, it’s another story to convince your coworkers! Your team may even adopt a style guide that prohibits it!

0:33

Operators shouldn’t be the bottleneck to introducing composition to your code. In this episode we’ll explore a few alternatives that may act as a more gentle introduction to the team and may even be the first step to getting your coworkers first-class tickets on the operator express! Outroducing |>

0:57

In our first episode we explored free functions and how, when they become nested, they become more difficult to read. incr(2) // 3 square(incr(2)) // 9

1:09

The first operator we introduced was |> , which was important for making free functions a bit more readable when called. Rather than passing a value to the right-hand side of a function, we pipe a value through it. 2 |> incr |> square // 9

1:19

This is a simple examples, but it’s far easier to read left-to-right, like method syntax, than to dive in via parentheses-matching, and follow the logic from inside-out.

1:22

The Swift community has adopted various non-operator solutions for function application in the past, though it may not have been so apparent! There was even a Swift Evolution pitch !

1:33

It’s the with function, which is often used for configuration. let label = UILabel() with(label) { $0.numberOfLines = 0 $0.systemFont(ofSize: 17) $0.textColor = .red }

1:45

But we also could have used our |> operator to do this: let label = UILabel() label |> { $0.numberOfLines = 0 $0.systemFont(ofSize: 17) $0.textColor = .red }

1:52

In other words, with has the perfect shape for function application.

1:59

Let’s define it. func with<A, B>(_ a: A, _ f: (A) -> B) -> B { return f(a) }

2:19

And let’s use it. with(2, incr) // 3

2:27

This works, but we can’t really chain our calls. Using an operator like |> allowed us to specify associativity and precedence in order to chain our calls along. with(2, incr, square) // extra argument in call

2:53

Applying two functions currently requires nesting and two with s. with(with(2, incr), square)

3:01

Now we’re dealing with nesting and parentheses, which is something that operators solved. Maybe one way to solve this is to somehow compose incr and square so that they can be fed to with just once. Outroducing >>>

3:20

Well, we may remember that a series of |> s can be factored using function composition! 2 |> incr |> square // 9 2 |> incr >>> square // 9

3:32

And with with instead of |> , we get this: with(2, incr >>> square) // 9

3:39

Using >>> allows us to plug these functions together before piping our value through.

3:45

Let’s define a free function for forward composition. We’ll call it pipe . Let’s not confuse this with the “pipe-forward” of |> ! We’re relying on prior art here by adopting the name of a function that the functional JavaScript community has rallied behind. func pipe<A, B, C>(_ f: @escaping (A) -> B, _ g: @escaping (B) -> C) -> (A) -> C { return { g(f($0)) } }

4:13

Now we can compose incr and square . pipe(incr, square) // (Int) -> Int This gives us a whole new function that increments and then squares a value.

4:19

We can even use with with this function. with(2, pipe(incr, square)) // 9

4:24

And it works, just like our operator version. We’re trading operators for words. We can read this as: with 2, pipe it into increment and square.

4:32

Like |> , the >>> operator also had associativity and precedence, allowing us to compose larger pipelines. incr >>> square >>> String.init // (Int) -> String

4:44

Unfortunately, pipe has the same issue with has. It only composes two functions at a time. pipe(incr, square, String.init) Extra argument in call

4:54

This is a problem that named functions have that operators don’t.

5:04

We can fix this, though, with function overloading. Let’s overload a version of pipe that supports three arguments. func pipe<A, B, C, D>( _ f: @escaping (A) -> B, _ g: @escaping (B) -> C, _ h: @escaping (C) -> D ) -> (A) -> D { return { h(g(f($0))) } }

5:23

And now, we can use pipe with three arguments. with(2, pipe(incr, square, String.init)) // "9"

5:26

If we want a pipe that composes four functions, we can write another overload. Writing these overloads may feel tedious, but we only have to write them once! We could even use a source code generation tool to automate the process.

5:45

In the future, we may even get language support! The Generics Manifesto outlines how variadic generics may allow us to define these sorts of functions without the need for overloads. // func pipe<A...>

6:12

We’ve also seen that operators work nicely over multiple lines. 2 |> incr >>> square >>> String.init // "9"

6:24

What’s this look like with with ? with(2, pipe( incr, square, String.init )) // "9"

6:31

This is a bit noisier, but reads pretty well overall.

6:42

Now, it’s kind of funny how we earlier pointed out that we named both with and pipe based on “prior art.” It seems that highly reusable free functions may also want to tick some boxes for justification. Luckily, we’ve justified the existence of these functions in the past! We can just swap out “does it have a nice shape” for “does it have a nice ring to it.” Outroducing >=>

7:16

In our episode on side effects , we introduced the “fish” operator, >=> , for composing functions that chain into a more complicated structure.

7:27

For example, we had a computeAndPrint function that would append logs to the return value in a tuple. func computeAndPrint(_ x: Int) -> (Int, [String]) { let computation = x * x + 1 return (computation, ["Computed \(computation)"]) } 2 |> computeAndPrint // (5, ["Computed 5"])

7:34

It was nice to be able to control our side effects, but we lost composition in the process. 2 |> computeAndPrint |> computeAndPrint Cannot convert value of type ‘(Int) -> (Int, [String])’ to expected argument type ‘(_) -> _’

7:55

Luckily, >=> restored composition by knowing how to piece these functions together by accumulating logs along the way. 2 |> computeAndPrint >=> computeAndPrint // (26, ["Computed 5", "Computed 26"])

8:10

We need a non-operator fish replacement. Let’s define a function called chain for this kind of composition. func chain<A, B, C>( _ f: @escaping (A) -> (B, [String]), _ g: @escaping (B) -> (C, [String]) ) -> ((A) -> (C, [String])) { return { a in let (b, logs) = f(a) let (c, moreLogs) = g(b) return (c, logs + moreLogs) } }

8:35

Let’s try it out! with(2, chain(computeAndPrint, computeAndPrint)) // (26, ["Computed 5", "Computed 26"]) It works! Like pipe , though, we have to overload this whenever we want to chain more functions together. Without overloads we have to do something like this with(2, chain(computeAndPrint, chain(computeAndPrint, computeAndPrint))) // (677, ["Computed 5", "Computed 26", "Computed 677"])

9:07

Another thing we mentioned in our episode on side effects was that >=> and >>> compose nicely together. 2 |> computeAndPrint >=> incr >>> computeAndPrint >=> square >>> computeAndPrint // (1874162, ["Computed 5", "Computed 37", "Computed 1874162"])

9:30

And this was great because we get a very visual way of seeing where the pure and effectful logic lives.

9:38

What’s this look like with named functions? with( 2, chain( computeAndPrint, pipe( incr, chain( computeAndPrint, pipe( square, computeAndPrint ) ) ) ) ) // (1874162, ["Computed 5", "Computed 37", "Computed 1874162"])

10:06

Uh oh. Operators kill this kind of nested, parentheses problem. But because chain only takes 2 functions, we’re forced into this kind of construction.

10:24

Let’s define an overload of chain that takes 3 functions. func chain<A, B, C, D>( _ f: @escaping (A) -> (B, [String]), _ g: @escaping (B) -> (C, [String]), _ h: @escaping (C) -> (D, [String]) ) -> ((A) -> (D, [String])) { return chain(f, chain(g, h)) }

11:07

Now we can refactor by grouping the pure and impure functions in a particular way: with(2, chain( computeAndPrint, pipe(incr, computeAndPrint), pipe(square, computeAndPrint) ))

11:28

It’s looking better, but we’re gonna have to keep defining overloads if we want to flatten parentheses. And even though parentheses have been flattened, the resulting expression is visually quite different from what we started with. This can make it more difficult to see the algebraic relations between pipe and chain . Outroducing <>

11:49

There’s one more composition we introduced early on in our series with the <> operator: single-type composition. For instance, we used it in our episode on UIKit styling. func roundedStyle(_ view: UIView) { view.clipsToBounds = true view.layer.cornerRadius = 6 } let baseButtonStyle: (UIButton) -> Void = { $0.contentEdgeInsets = UIEdgeInsets( top: 12, left: 16, bottom: 12, right: 16 ) $0.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) } let roundButtonStyle = baseButtonStyle <> roundedStyle

12:11

As real-world as this stuff was, the operator alone may prevent this kind of powerful composition from entering some code bases. remember that we want to restrict composition a bit more, as we did with <> . Let’s define a function called concat for the version we used in our styling episode. func concat<A: AnyObject>( _ f: @escaping (A) -> Void, _ g: @escaping (A) -> Void ) -> (A) -> Void { return { a in f(a) g(a) } } We took concat from the same prior art as pipe and chain , but we could have easily called this append instead.

13:03

Let’s use it. let roundButtonStyle = concat( baseButtonStyle, roundedStyle ) // (UIButton) -> Void

13:22

It even works nicely with trailing closure syntax. let filledButtonStyle = concat(roundedButtonStyle) { $0.backgroundColor = .black $0.tintColor = .white }

13:39

With concat we can even avoid the need to overload! The type’s the same so our signature can be variadic! Let’s ensure that we pass at least two functions to concat, but allow for any number of additional functions. func concat<A: AnyObject>( _ f1: @escaping (A) -> Void, _ f2: @escaping (A) -> Void, _ fs: ((A) -> Void)... ) -> (A) -> Void { return { a in f1(a) f2(a) fs.forEach { f in f(a) } } }

14:56

Now we have a single concat function and we never have to overload it for additional inputs! We could paste this single function into a codebase and gain access to this kind of styling function composition immediately.

15:12

This breaks trailing closures, but seems like an acceptable trade-off if we can avoid a bunch of overloads. let filledButtonStyle = concat(roundedButtonStyle, { $0.backgroundColor = .black $0.tintColor = .white })

15:23

Let’s see how we can continue to concat things along. let filledButtonStyle = concat( baseButtonStyle, roundedButtonStyle, { $0.backgroundColor = .black $0.tintColor = .white } ) It’s nice to see that concat doesn’t have any of the overloading issues we had with pipe and chain . Algebraic properties

15:53

One thing we stressed while using infix operators in the past is that they help us see algebraic relations that can result in real world applications, such as performance improvements.

16:11

Let’s take a look at a common operator property that we’re used to from math. a * (b + c) == a*b + a*c Here we see that multiplication distributes over addition, and operators are a really good way of showing this relation.

16:25

We saw this in our operators, as well! They allowed us to see that map distributes over composition: map(f >>> g) = map(f) >>> map(g)

16:32

We also saw something similar in our episode on setters, where they too distribute over composition: first(f >>> g) = first(f) >>> first(g)

16:42

We also have the following equation relating pipe forward with forward compose: (a |> f) |> g = a |> (f >>> g)

17:00

With named functions this becomes a little bit harder, but it’s still possible! For example, the map property with respect to composition becomes: map(pipe(f, g)) = pipe(map(f), map(g))

17:13

In words: “The map of a pipe is the pipe of the maps!” The first property looks about the same: first(pipe(f, g)) = pipe(first(f), first(g))

17:30

The function application property looks like: with(with(a, f), g) = with(a, pipe(f, g))

17:45

So, essentially we can flatten two with calls at the cost of introducing a pipe . Seems reasonable!

18:04

What about associativity? We saw that >>> and >=> were both associative in that it did not matter where we put parentheses. (f >>> g) >>> h = f >>> (g >>> h) (f >=> g) >=> h = f >=> (g >=> h)

18:34

With named functions this looks like: pipe(f, pipe(g, h)) = pipe(pipe(f, g), h) chain(f, chain(g, h)) = chain(chain(f, g), h)

19:00

So we can see that operators help clear the fog when searching for algebraic relations between functions and constructions, but once you know those relations it is easy enough to transport it back to named functions and manipulate them abstractly. What’s the point?

19:21

Typically, we ask “what’s the point?” because we explore some pretty wild stuff, introducing operators or some strange composition, that might not seem practical or applicable and we want to ground the concepts in pragmatism. This time we want to flip that question and ask “what’s the point?” of taking these beautiful operators and giving them human names? Why do that?!

20:07

Composition! Most of our series distills down to exploring how powerful composition can be. It’d be a shame to miss out on it! By adopting named functions, we can introduce these concepts more gradually into code bases that might have otherwise missed out. We still love (and prefer) custom operators, but if we had to pick between operators and composition, it’d be composition! Most of our operators rely on composition, so if we didn’t have composition, we wouldn’t even have our favorite operators. We have to give operators credit, though, for cleaning up parentheses problems with associativity and precedence, and allowing us to avoid writing (or generating) a bunch of overloads every time. References Swift Overture Brandon Williams & Stephen Celis • Apr 9, 2018 We open sourced the Overture library to give everyone access to functional compositions, even if you can’t bring operators into your codebase. https://github.com/pointfreeco/swift-overture Downloads Sample code 0011-composition-without-operators 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 .