EP 40 · Standalone · Dec 17, 2018 ·Members

Video #40: Async Functional Refactoring

smart_display

Loading stream…

Video #40: Async Functional Refactoring

Episode: Video #40 Date: Dec 17, 2018 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep40-async-functional-refactoring

Episode thumbnail

Description

The snapshot testing library we have been designing over the past few weeks has a serious problem: it can’t snapshot asynchronous values, like web views and anything that uses delegates or callbacks. Today we embark on a no-regret refactor to fix this problem with the help of a well-studied and well-understood functional type that we have discussed numerous times before.

Video

Cloudflare Stream video ID: 8fe929a911bb4c142a96d8034ea4b3fb Local file: video_40_async-functional-refactoring.mp4 *(download with --video 40)*

References

Transcript

0:05

We’ve spent the past few weeks working on a real-world library in order to explore the ins-and-outs of protocol-oriented programming ( part 1 , part 2 , part 3 ). We saw that we can improve the designs of our APIs by scrapping our protocols and using concrete data types, a process we explored in depth in our series on protocol witnesses.

0:25

While the library we’ve built is already incredibly powerful and flexible, it has a serious limitation: it can’t snapshot values that are asynchronous. This is a pretty big limitation since many values can’t be snapshot until a callback is invoked or a delegate method is called, and in fact it completely prevents us from writing snapshot tests for web views!

0:41

This was a real problem we encountered in using this library because we take snapshots of web views to write tests for this very site, which is written in Swift and completely open source. Web views have this exact problem, where they require waiting for a delegate method to be called and a callback to be invoked to produce a snapshot image.

1:12

The solution turned out to be incredibly simple and we did it in a matter of minutes. The best part was that our refactor was completely guided by some of the functional programming ideas we’ve covered on this very site. In fact, a type we’ve discussed a number of times on this series came to the rescue.

1:30

So today we want to show why this async snapshot thing really is a problem with the current library design, and show how a to solve this in a really nice, simple way. Snapshotting web views

1:47

We’ve stubbed out a simple test that instantiates a web view and loads some HTML into it. We can write a snapshot assertion against the web view using our assertSnapshot helper. Because WKWebView is a subclass of UIView , we can even reuse our image snapshot strategy. class PointFreeFrameworkTests: XCTest { func testWebView() { let html = """ <h1>Welcome to Point-Free!</h1> <p> A Swift video series exploring functional programming and more. </p> """ let webView = WKWebView( frame: CGRect(x: 0, y: 0, width: 640, height: 480) ) webView.loadHTMLString(html, baseURL: nil) } }

2:11

When we run our test, we get a failure: failed - Recorded: … “…/PointFreeFrameworkTests/__Snapshots__/PointFreeFrameworkTests/testWebView.png”

2:16

This is expected because there was no existing reference on disk. It recorded a brand new snapshot. Let’s take a look at it.

2:21

It’s blank! What gives!? We’ve hit a problem where not only has our web view not fully loaded, but WKWebView is one of a handful of UIView subclasses that cannot be rendered to an image the traditional, layer-based way. Instead, it provides its own API: a takeSnapshot method that takes a callback block with an optional image and optional error. // assertSnapshot(matching: webView, as: .image) webView.takeSnapshot(with: nil) { image, error in }

3:04

Inside this block we can call assertSnapshot against the UIImage rather than the view. webView.takeSnapshot(with: nil) { image, error in self.assertSnapshot(matching: image!, as: .image) }

3:26

Now when we run our test, we get a crash: Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value The image is nil for some reason. If we examine the error , it isn’t particularly helpful. (lldb) po error ▿ Optional<Error> - some : Error Domain=WKErrorDomain Code=1 "An unknown error occurred" UserInfo={NSLocalizedDescription=An unknown error occurred}

3:44

Web views load asynchronously, but we’ve synchronously attempted to take a snapshot of a web view while it’s still loading. What we need to do is wait for the web view to be ready for its snapshot to be taken.

3:55

WKWebView has a navigation delegate that allows us to do just that. The navigation delegate is sent a callback message when the web view has finished rendering.

4:02

Let’s write a simple delegate class and stub out the method we care about. class NavigationDelegate: NSObject, WKNavigationDelegate { func webView( _ webView: WKWebView, didFinish navigation: WKNavigation! ) { <#code#> } }

4:29

What goes in this method? We want it to notify our test code that the web view is ready to be snapshot. What we can do is capture this work in a property that is invoked from the delegate method. class NavigationDelegate: NSObject, WKNavigationDelegate { let callback: () -> Void init(callback: @escaping () -> Void) { self.callback = callback } func webView( _ webView: WKWebView, didFinish navigation: WKNavigation! ) { self.callback() } }

5:00

This is very general way of converting delegate methods into callbacks.

5:16

Now we can instantiate a delegate, and do our work inside its callback block, and assign the delegate. let delegate = NavigationDelegate.init(callback: { webView.takeSnapshot(with: nil) { image, error in self.assertSnapshot(matching: image!, as: .image) } }) webView.navigationDelegate = delegate

5:35

Now we’ve done all the work we need to do to ensure assertSnapshot is only called when the web view is ready.

5:47

We can now run the test and it no longer crashes…but why does it pass!? If the web page rendered and the assertion ran, shouldn’t it be different than the original, blank reference?

5:58

If we set a breakpoint inside the navigation delegate callback, it’s never getting hit. This is because the test is completing before the callback runs. We can use XCTest expectations to wait for our asynchronous work before letting our test complete. let loaded = expectation(description: "loaded") let delegate = NavigationDelegate { webView.takeSnapshot(with: nil) { image, error in loaded.fulfill() self.assertSnapshot(matching: image!, as: .image) } } webView.navigationDelegate = delegate wait(for: [loaded], timeout: 5) We create an expectation, fulfill it inside the block, and wait will block the test till fulfill is called.

6:59

Now when we run our test, the breakpoint hits, and we get a failure! failed - Expected old@(640.0, 480.0) to match new@(640.0, 480.0)

7:12

If we inspect the output in the report navigator, sure enough the new snapshot is rendering a beautiful web page as expected.

7:24

Let’s record this version for posterity. record = true failed - Recorded: … “…/PointFreeFrameworkTests/__Snapshots__/PointFreeFrameworkTests/testWebView.png”

7:32

When we turn off record mode and re-run the test, it passes!

7:39

And we can verify that the new reference on disk indeed reflects the HTML content. A web view strategy

7:45

Alright, we got things to work, but it took a lot of work to get there, and the end result doesn’t look so nice. Rather than use our assertSnapshot helper inline, we needed to use WKWebView ’s takeSnapshot API, and to do so we needed to create a navigation delegate class, instantiate it, remember to assign it, and only then call assertSnapshot against the image inside a nest of callbacks. And this work needs to be done every single time we want to snapshot a web view.

8:24

We could maybe write a helper function to reuse throughout our test suite and call it a day. But it feels a bit wrong to solve it that way. This seems like something that our ultra-flexible little library should be able to do for us. We have all these Snapshotting strategies at our disposal, so shouldn’t we be able to write a new strategy for web views?

8:41

Ideally we should be able to write assertSnapshot(matching: webview, as: .image) and it should all just work. We should be able to write an image strategy for WKWebView that bundles up all this work so we don’t have to think about it.

8:55

Whenever you want to make a new, custom snapshot strategy, you have two choices: you can either build a value of the Snapshotting type from scratch, providing the diffing strategy, path extension, and a snapshot function, or (because this is a lot of work) you can take an existing strategy and use the pullback operation to take the strategy’s type and pull it back to work with another type as long as the other type can map into the existing strategy’s type.

9:35

We know how to snapshot images, so let’s see if we can pull it back to work with web views.

9:48

Let’s reopen Snapshotting constrained against WKWebView and UIImage and see if we can define another strategy. We can use pullback on our UIImage strategy to define things. extension Snapshotting where A == WKWebView, Snapshot == UIImage { static let image: Snapshotting = Snapshotting<UIImage, UIImage> .image.pullback { webView in } } As long as we can convert our web view to an image in this block, everything else should just work.

11:05

So what goes in here? Well, let’s copy and paste our earlier logic and try to get things to work. extension Snapshotting where A == WKWebView, Snapshot == UIImage { static let image: Snapshotting = Snapshotting<UIImage, UIImage> .image.pullback { webView in let loaded = expectation(description: "loaded") let delegate = NavigationDelegate { webView.takeSnapshot(with: nil) { image, error in loaded.fulfill() self.assertSnapshot(matching: image!, as: .image) } } webView.navigationDelegate = delegate wait(for: [loaded], timeout: 5) } } Use of unresolved identifier ‘expectation’ Use of unresolved identifier ‘self’

11:15

We’re no longer inside a test case, so expectation , wait , and assertSnapshot have to go. extension Snapshotting where A == WKWebView, Snapshot == UIImage { static let image: Snapshotting = Snapshotting<UIImage, UIImage> .image.pullback { webView in // let loaded = expectation(description: "loaded") let delegate = NavigationDelegate { webView.takeSnapshot(with: nil) { image, error in // loaded.fulfill() // self.assertSnapshot(matching: image!, as: .image) } } webView.navigationDelegate = delegate // wait(for: [loaded], timeout: 5) } } Missing return in a closure expected to return ‘UIImage’

11:32

What we need to do is somehow get the image inside the callback block and return it from the top level. Maybe we can create an implicitly unwrapped image, assign it inside the inner-most block, and finally return it at the end of the outer block. extension Snapshotting where A == WKWebView, Snapshot == UIImage { static let image: Snapshotting = Snapshotting<UIImage, UIImage> .image.pullback { webView in var webViewImage: UIImage! // let loaded = expectation(description: "loaded") let delegate = NavigationDelegate() { webView.takeSnapshot(with: nil) { image, error in webViewImage = image! // loaded.fulfill() // self.assertSnapshot(matching: image!, as: .image) } } webView.navigationDelegate = delegate // wait(for: [loaded], timeout: 5) return webViewImage } }

11:58

The compiler is happy, but this still isn’t going to work because we’re no longer waiting for the page to load using a test expectation, so this code will immediately crash while trying to unwrap our nil image.

12:11

We need something expectation-like, so let’s use Grand Central Dispatch’s semaphore type, which is works in a similar way and allows us to wait for some asynchronous work to occur. extension Snapshotting where A == WKWebView, Snapshot == UIImage { static let image: Snapshotting = Snapshotting<UIImage, UIImage> .image.pullback { webView in let sema = DispatchSemaphore.init(value: 0) var webViewImage: UIImage! let delegate = NavigationDelegate { webView.takeSnapshot(with: nil) { image, error in webViewImage = image sema.signal() } } webView.navigationDelegate = delegate sema.wait() return webViewImage } } It ends up looking a like the work we were doing with expectations before, but now uses a different API that we have access to.

12:49

Let’s try our strategy out by swapping out our ad hoc logic for a single call to assertSnapshot which bundles up all the work we wrote for our strategy. assertSnapshot(matching: webView, as: .image)

13:14

So let’s run it… …

13:19

Huh. The tests are still running but nothing is happening. We must be deadlocked! If we pause execution, we can see that we’re sitting on sema.wait() , so we must never be signaling.

13:45

The problem is we’re running on the main queue inside our strategy, and takeSnapshot is invoked on the main queue. So it seems impossible for our callback to ever execute. Async snapshotting

14:08

This is exactly the problem we hit when building our library when we tried to write snapshot tests against the Point-Free web site: deadlocks.

14:23

Grand Central Dispatch just isn’t going to help here. We need to use XCTest’s expectation API. It’s the way to write tests around asynchronous work.

14:34

So how can we leverage those XCTestCase APIs? Do we bake them into our Snapshotting strategy? That seems messy.

14:41

What we need is the ability for our Snapshotting type to express the idea of taking an asynchronous snapshot, and then our assertSnapshot helper could manage the expectation and wait logic. The assertSnapshot helper is already heavily coupled to XCTest, so it seems like the appropriate place to do this work.

15:00

So what does it mean to express the idea of asynchronous work? We’ve covered a type on this series, several times (in our episodes on map and our series on zip ), that expresses just that! The Parallel type. It’s a wrapper around a function with a bizarre signature, and we saw that this type had some interesting means of composition, including a zip that allowed us to express the idea of running the work of several Parallel s at the same time.

15:26

Let’s revisit the Parallel type and see how it may help us.

15:33

First, let’s hop over to the Snapshotting type and remind ourselves how it works. struct Snapshotting<A, Snapshot> { let diffing: Diffing<Snapshot> let pathExtension: String let snapshot: (A) -> Snapshot func pullback<A0>( _ f: @escaping (A0) -> A ) -> Snapshotting<A0, Snapshot> { return Snapshotting<A0, Snapshot>( diffing: self.diffing, pathExtension: self.pathExtension, snapshot: { a0 in self.snapshot(f(a0)) } ) } } It’s a concrete type that holds a diffing strategy, a path extension, and a snapshot function: the thing that turns a value into something that can be snapshot and diffed. This is the function that is responsible for turning, for example, a WKWebView into a UIImage .

15:54

And this is where our problem lies! The snapshot function is synchronous: given an A it will immediately return a Snapshot . This is what needs to be modified to be asynchronous so that we can avoid that deadlock.

16:07

Let’s redefine the Parallel type. It’s a wrapper around a function that takes a callback from (A) -> Void and returns Void . struct Parallel<A> { let run: (@escaping (A) -> Void) -> Void }

16:22

This type is the distillation of what it means to have an asynchronous value. For example, to represent an asynchronous Int , you can create a parallel and, in the body of the parallel function you invoke the callback with a number: let x = Parallel<Int> { callback in callback(42) }

16:45

Because you can call this callback at any time, you can do whatever asynchronous work you want so long that the callback is invoked after the work completes.

16:56

To get the value, you need to run it, and only when you run it can you gain access to the value inside. x.run { $0 }

17:14

Now that we’ve captured what it means to be async in a type, the fix for our problem is very simple: we merely change our Snapshotting ’s snapshot function from returning a Snapshot to returning a Parallel<Snapshot> instead. let snapshot: (A) -> Parallel

17:34

This breaks some things because all of our existing snapshotting strategies are defined in synchronous terms, but we just have to go fix our errors and we should be able to solve our original problem.

17:41

Our first error is that we’re describing how to snapshot UIImage s with a snapshot function that returns the image when it now needs to return a parallel image. extension Snapshotting where A == UIImage, Snapshot == UIImage { static let image = Snapshotting( diffing: .image, pathExtension: "png", snapshot: { $0 } ) }

17:54

Let’s update our synchronous function to be asynchronous. extension Snapshotting where A == UIImage, Snapshot == UIImage { static let image = Snapshotting( diffing: .image, pathExtension: "png", snapshot: { image in Parallel { callback in callback(image) } } ) } And all this nesting really just expresses the ability to take a synchronous value and wrap it up in an asynchronous context.

18:28

All of our snapshot strategies so far are technically synchronous, so we need to update all of them to be asynchronous in the same fashion. But rather than update every existing witness, let’s define a convenience initializer that wraps the snapshot logic up in a Parallel .

18:45

We need to put this initializer in an extension so we don’t lose the default initializer Swift generates for us. extension Snapshotting { init( diffing: Diffing<Snapshot>, pathExtension: String, snapshot: @escaping (A) -> Snapshot ) { self.diffing = diffing self.pathExtension = pathExtension self.snapshot = { a in Parallel { callback in callback(snapshot(a)) } } ) }

19:58

Alright, that fixed most of the errors and we can simplify our image snapshot by reverting our earlier change to use the synchronous interface. extension Snapshotting where A == UIImage, Snapshot == UIImage { static let image = Snapshotting( diffing: .image, pathExtension: "png", snapshot: { $0 } ) }

20:14

We have a couple more errors to handle in assertSnapshot . guard let (failure, attachments) = witness.diffing .diff(reference, snapshot) else { return } … try! witness.diffing.data(snapshot).write(to: referenceUrl) Cannot invoke value of type ‘(Snapshot, Snapshot) -> (String, [XCTAttachment])?’ with argument list ‘(Snapshot, Parallel<Snapshot>)’

20:28

The error occurs when we try to diff a reference with a snapshot, and this is because snapshot is now wrapped up in a Parallel . We need to unwrap it before we pass it to his function.

20:51

First, we can rename snapshot to parallel , run it, and assign it to an implicitly unwrapped optional like before. let parallel = witness.snapshot(value) var snapshot: Snapshot! parallel.run { snapshot = $0 }

21:27

The errors went away, but we’re not quite done yet, because we’re not waiting for the parallel work to happen, and that means that snapshot might be nil when we need it later. We need to hook into the expectation API we used earlier. let parallel = witness.snapshot(value) var snapshot: Snapshot! let loaded = self.expectation(description: "loaded") parallel.run { snapshot = $0 loaded.fulfill() } wait(for: [loaded], timeout: 5)

22:05

This unit of code is exactly what we were doing before in a more ad hoc fashion, but we’ve now abstracted away the notion of web views, navigation delegates, and the takeSnapshot API and instead are representing just the work it means to run any asynchronous work using XCTest expectations.

22:28

That didn’t take much work! We merely identified the problem: we had a deadlock and needed to manage some async work using XCTest’s APIs. We introduced the notion of an async value into our Snapshotting API. Finally, we bundled up the expectation logic into a very small part of our assertSnapshot helper, which was already heavily coupled to XCTest, and that should fix basically everything.

23:06

And most of our code didn’t need to change. Existing strategies kept on working with the help of a convenience initializer. We merely expanded the functionality of our Snapshotting type. An async web view strategy

23:18

We’re not done yet, because we still haven’t updated our web view strategy to use the Parallel type. extension Snapshotting where A == WKWebView, Snapshot == UIImage { static let image: Snapshotting = Snapshotting<UIImage, UIImage> .image.pullback { webView in let sema = DispatchSemaphore.init(value: 0) var webViewImage: UIImage! let delegate = NavigationDelegate { webView.takeSnapshot(with: nil) { image, error in webViewImage = image sema.signal() } } webView.navigationDelegate = delegate sema.wait() return webViewImage } }

23:36

We’re currently defining this strategy using a pullback from the existing UIImage strategy, but pullback is synchronous, so let’s use the async initializer instead. static let image = Snapshotting( diffing: .image, pathExtension: "png" ) { webView -> Parallel<UIImage> in return Parallel<UIImage> { callback in let delegate = NavigationDelegate { webView.takeSnapshot(with: nil) { image, error in callback(image!) } } webView.navigationDelegate = delegate } } Our transform function needed to be updated to return a Parallel<UIImage> instead of a UIImage , get rid of the GCD logic, and get rid of the implicitly unwrapped image.

25:02

Now our strategy very cleanly describes the parallelism using the Parallel type. We ended up deleting a lot of code since it’s now baked into assertSnapshot .

25:11

Now when we run our test… …

25:18

We get a crash? Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value Huh, snapshot is nil ? What’s going on? If we look at our console, it looks like our expectation timed out and our parallel never ran.

25:36

Huh, our callback never ran. Let’s look at our strategy again. snapshot: { webView in Parallel<UIImage> { callback in let delegate = NavigationDelegate { webView.takeSnapshot(with: nil) { image, error in callback(image!) } } webView.navigationDelegate = delegate } } We’re not doing a ton of work here, but the work we are doing has a subtle gotcha: WKWebView ’s navigationDelegate is weak. weak open var navigationDelegate: WKNavigationDelegate?

25:57

That means that something needs to hold onto a reference of this delegate for it to live longer than the execution of the parallel block. Currently, the delegate goes away as soon as it’s assigned, so it’s nil by the time the delegate method fires, so the parallel callback can never be called.

26:12

What we want to do is capture and retain the delegate in the deepest callback block. snapshot: { webView in Parallel<UIImage> { callback in let delegate = NavigationDelegate { webView.takeSnapshot(with: nil) { image, error in callback(image!) _ = delegate } } webView.navigationDelegate = delegate } } Variable used within its own initial value

26:21

We can’t refer to delegate here because we haven’t instantiated it yet. We need to decouple the navigation delegate’s initializer from its callback. class NavigationDelegate: NSObject, WKNavigationDelegate { var callback: (() -> Void)? func webView( _ webView: WKWebView, didFinish navigation: WKNavigation! ) { self.callback!() } } We can delete the initializer and rely on NSObject ’s, and we can make callback an optional property that can be assigned later.

26:48

Now we just need to modify our strategy with the delegate’s class changes. static let image = Snapshotting( diffing: .image, pathExtension: "png", snapshot: { webView in Parallel<UIImage> { callback in let delegate = NavigationDelegate() delegate.callback = { webView.takeSnapshot(with: nil) { image, error in callback(image!) _ = delegate } } webView.navigationDelegate = delegate } } )

27:00

It builds! And when we run tests, they pass!

27:10

This means assertSnapshot ran, produced an image, diffed it against our reference on disk to verify it hasn’t changed.

27:21

Let’s change the HTML a bit to make sure things fail appropriately. failed - Expected old@(640.0, 480.0) to match new@(640.0, 480.0)

27:30

We took a library that was already incredibly flexible and extensible and we just layered an entire new dimension of flexibility on top of it. We were able to bake in some gnarly asynchronous behavior using a very simple, composable type that we were already familiar with. Async pullback

27:52

Let’s address something unfortunate.

28:00

When we redefined the web view strategy using Parallel , we did so from scratch because pullback doesn’t know about Parallel , and we required access to Parallel to define the callback-based strategy. We’ve lost some composition and we’re back to hard-coding diffing and pathExtension .

28:18

The pullback was such a nice means of building new strategies out of old ones, but we’ve completely lost the ability to do so with async strategies! Can we recover this functionality? Is it possible to define an asyncPullback ? func asyncPullback<A0>( _ f: @escaping (A0) -> Parallel<A> ) -> Snapshotting<A0, Snapshot> { } Alright, in order to be async we need to return a Parallel instead of an A , but otherwise the signature is the same.

29:11

What about the body of the function? Well, we know we need to return a Snapshotting<A0, Snapshot> , and we know we can pass diffing and pathExtension along. func asyncPullback<A0>( _ f: @escaping (A0) -> Parallel<A> ) -> Snapshotting<A0, Snapshot> { return Snapshotting<A0, Snapshot>( diffing: self.diffing, pathExtension: self.pathExtension, snapshot: <#(A0) -> Parallel<Snapshot>#> ) }

29:34

So how do we implement snapshot ? Well we know we need to return a Parallel<Snapshot> , so let’s define it and open it up with the callback that takes Snapshot s. func asyncPullback<A0>( _ f: @escaping (A0) -> Parallel<A> ) -> Snapshotting<A0, Snapshot> { return Snapshotting<A0, Snapshot>( diffing: self.diffing, pathExtension: self.pathExtension, snapshot: { a0 in return Parallel<Snapshot> { callback in <#???#> } } ) }

29:48

Now we somehow need to get a hold of a Snapshot to pass to the callback? Let’s see what we’re working with. func asyncPullback<A0>( _ f: @escaping (A0) -> Parallel<A> ) -> Snapshotting<A0, Snapshot> { return Snapshotting<A0, Snapshot>( diffing: self.diffing, pathExtension: self.pathExtension, snapshot: { a0 in Parallel<Snapshot> { callback in // callback // (Snapshot) -> Void // a0 // A0 // f // (A0) -> Parallel<A> // self.snapshot // (A) -> Snapshot } } ) }

30:24

These are all the values we need to work with, we just need to connect them all together wherever the types match up so that a Snapshot gets passed to callback ! func asyncPullback<A0>( _ f: @escaping (A0) -> Parallel<A> ) -> Snapshotting<A0, Snapshot> { return Snapshotting<A0, Snapshot>( diffing: self.diffing, pathExtension: self.pathExtension, snapshot: { a0 in Parallel<Snapshot> { callback in // callback // (Snapshot) -> Void // a0 // A0 // f // (A0) -> Parallel<A> // self.snapshot // (A) -> Snapshot let parallelA = f(a0) parallelA.run { a in let parallelSnapshot = self.snapshot(a) parallelSnapshot.run { snapshot in callback(snapshot) } } } } ) } We pass a0 to f to get a Parallel<A> . Then we can call run on the parallel to get access to its A . Finally we can call self.snapshot with that A to get a Parallel<Snapshot> , which we need to run in order to access its Snapshot and finally pass it to our root callback. Whew!

31:17

Everything builds, and now we have this brand new asyncPullback operation at our disposal that lets us pull back existing strategies onto new types that require some asynchronous work.

31:39

So with all that, we should now be able to redefine our web view strategy using asyncPullback ! static let image: Snapshotting = Snapshotting<UIImage, UIImage> .image.asyncPullback { webView in Parallel { callback in let delegate = NavigationDelegate() delegate.callback = { webView.takeSnapshot(with: nil) { image, error in callback(image!) _ = delegate } } webView.navigationDelegate = delegate } }

32:09

And this is really cool. We discovered a brand new async version of pullback that allowed us to more succinctly build async strategies out of existing strategies. And solving the problem ended up being very straightforward and familiar. What’s the point?

32:34

It’s about time we asked “what’s the point?” What was the point of walking through this refactor?

32:42

On one level, we had a nice library that had a problem, so it’s nice to see what goes into refactoring the problem away.

32:52

But what’s maybe more interesting is that functional refactors like these can be thought of as a “no regret” refactor. We didn’t compromise the purity of the library or pave over any of the other wonderful features the library had. We found a way of refactoring our library that retained everything nice about the library while supporting a brand new use case.

33:15

And we think that’s the reason why deep dives into understanding things like zip , Parallel , and all the seemingly abstract things we do starts to pay off. We could very confidently go in, know that we needed to change a single function signature from (A) -> Snapshot to (A) -> Parallel<Snapshot> , fix a few compiler errors, and everything just works.

33:37

So that’s the point! A functional refactor lets us confidently change code built from small, composable, well-understood units and not compromise the integrity of the functional code we started with. Because we designed our refactor around Parallel , a well-understood type, we are less likely to regret the design decisions we made later. Till next time! References Protocol-Oriented Programming in Swift Apple • Jun 16, 2015 Apple’s eponymous WWDC talk on protocol-oriented programming: At the heart of Swift’s design are two incredibly powerful ideas protocol-oriented programming and first class value semantics. Each of these concepts benefit predictability, performance, and productivity, but together they can change the way we think about programming. Find out how you can apply these ideas to improve the code you write. https://developer.apple.com/videos/play/wwdc2015/408/ uber/ios-snapshot-test-case Uber, previously Facebook Facebook released a snapshot testing framework known as FBSnapshotTestCase back in 2013, and many in the iOS community adopted it. The library gives you an API to assert snapshots of UIView ’s that will take a screenshot of your UI and compare it against a reference image in your repo. If a single pixel is off it will fail the test. Since then Facebook has stopped maintaining it and transfered ownership to Uber. https://github.com/uber/ios-snapshot-test-case Snapshot Testing in Swift Stephen Celis • Sep 1, 2017 Stephen gave an overview of snapshot testing, its benefits, and how one may snapshot Swift data types, walking through a minimal implementation. https://www.stephencelis.com/2017/09/snapshot-testing-in-swift Downloads Sample code 0040-async-functional-refactoring 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 .