0:06 In this episode, we're going to build upon the Future type we
implemented in a previous
episode. Specifically, we'll
use the Future type as a basis to build a simple reactive implementation.
While a future is an abstraction around a one-off callback, a signal in a
reactive library models values over time.
0:38 You'd use a future in an asynchronous API if the call only delivers
one result, e.g. most network calls or some expensive computation. In contrast,
you'd use a signal if you have to handle multiple values over time, like for a
button that can be tapped multiple times, or for a text field that changes its
value with each new user input. In the case of the signal, we have to think
about some additional issues, like memory management (to avoid reference
cycles), and subscription management (to allow clients to opt in and opt out of
receiving values of a particular signal).
1:17 The motivation behind writing a small reactive library from scratch
is really to understand the concept of reactive programming on a deeper level.
Our goal isn't to write production-ready code, but rather to wrap our heads
around what's going on behind the scenes when using one of the existing reactive
1:55 As a first step, we remove the caching of the result, because this
doesn't make sense for a signal anymore, at least not in this form. We also
remove the initializer, rename the type from Future to Signal, make send
public for now, and rename onResult to subscribe. Lastly, we don't clear out
the callbacks at the end of the send method, since we now want to be able to
send multiple values. This simplifies the code quite a bit:
4:30 The Signal implementation is now very simple, but also very
limited. We'll have to add back some more complexity once we build out this type
Separating Send/Subscribe APIs
4:49 The first enhancement we'll make is to separate the Signal's API
into a sending part and a receiving part. This is a very common pattern in
reactive libraries, because it allows you to control who can send new values
into the signal vs. who's only allowed to subscribe.
5:22 For this we add a static method, pipe, to Signal, which returns
those two things: a function with which we can send new values, and the signal
itself, which is read-only. You can take the name of the method quite literally:
the returned tuple can be thought of as a physical pipe with two ends. At one
end you can insert new values (also called the "sink"), and at the other end you
can observe what's coming out:
9:05 To make this code work, we have to add the signal method in an
extension to NSTextField. In this method, we can take advantage of the
send/subscribe separation we've created above — the text field is able to send
new values to the signal, while any other code can only observe:
10:23 In this approach, we don't want the text field to reference the
signal, but rather the signal should reference the text field (via the key-value
observer). This has the advantage that it's more flexible, since we'd have to
subclass the text field or use associated objects to store a reference to the
signal in the text field itself.
12:09 The KeyValueObserver class is a simple wrapper around KVO that
calls a function each time the observed property changes. It also manages the
observation lifetime: once the key-value observer instance goes away, it stops
observing the text field. As long as the key-value observer is alive, the
observed object is guaranteed to be around as well, since the observer instance
holds a strong reference to the observed object.
12:39 Since we're not yet holding on to the KeyValueObserver instance,
it gets deinited immediately and observation stops. The easiest way to fix this
is to just store the observer in a property of the signal. We'll call this
property objects for now:
15:16 Now the observer will be alive as long as the signal is alive.
Since the signal is stored in a property of the view controller, new values will
be sent as long as the view controller is around.
Dealing with Reference Cycles
15:33 Unfortunately, we accidentally introduced a reference cycle, which
makes it so that the signal will never be deallocated.
15:46 Reference cycles are difficult to debug sometimes, since many
objects can be involved in creating the cycle. A very simple technique to help
diagnosing these cycles is to add a print statement to a class' deinit. For
example, we can try this with our view controller class:
16:57 One very useful tool to further diagnose the problem is Xcode's
memory debugger. Since this doesn't work in a playground, we'll just copy/paste
our code into a command line project and continue working in there.
17:21 Now we set a break point after we've set the vc variable to
nil and run the project. Once the debugger stops on this line, we can open the
17:40 In the sidebar we see all the objects that are alive, e.g. the
Signal<String> instance that shouldn't be around anymore. On the right-hand
side, we see all the objects involved in the reference cycle that keeps the
signal alive: the signal references the key-value observer through its objects
property, and the key-value observer references the signal via a few more
18:16 This visualization can be very helpful in diagnosing the problem.
However, sometimes the memory debugger isn't able to generate a graph like this,
but it'll still show the objects that are alive in the sidebar.
Sketching Out Reference Cycles
19:04 Another helpful approach is to sketch out the references of all
the objects on a piece of paper. We do this a lot to understand how we've
created a reference cycle. In the example at hand, the diagram looks like this:
19:10 In the upper-left corner we have the view controller, which
strongly references the text field and the string signal (as indicated by the
arrows). The string signal references the closure with the print statement, as
well as the key-value observer it uses to observe the text field for changes.
The key-value observer in turn holds a strong reference to the text field.
19:40 This might already look like a reference cycle when you look at
the left part of the diagram (view controller – string signal – key-value
observer – text field – view controller). However, not all references are
pointing in the same direction, so we don't have a problem yet.
19:55 The key-value observer also references the closure that gets
called when the observed property changes. In our case, this is the function
that feeds the new values into the sink. In turn, the sink strongly references
the signal, since the sink is just the signal's send method. So here's our
Fixing the Reference Cycle
20:14 To fix the cycle, we have to think about which of the references
could be a weak reference. For example, we can't make the reference from the
signal to the observer weak, since the observer wouldn't be referenced strongly
by anything anymore. The same goes for the closure that's referenced by the
observer. Therefore, we're going to break the cycle by making the reference from
the closure to the signal weak:
20:40 For this, we have to modify our implementation of the Signal's
pipe method. Instead of returning signal.send, we're going to wrap this in a
closure that captures signal weakly:
21:50 With this minor change, the reference cycle is fixed, which we
can see from the printout in the console "Removing signal", as well as in the
22:31 In this case, the reference cycle was caused by a mistake in the
implementation of Signal. However, there are also cases where it's the
responsibility of the consumer of the API to not create cycles. For example, if
we were to refer to self in the closure we pass to the subscribe call in
viewDidLoad, we would've created a reference cycle between the view controller
and the signal. To avoid that, we'd have to specify weak self or unowned
self in the closure's capture list.
23:27 With this problem out of the way, we'll add a bit more
functionality to our Signal type next time!