Illustration taken from https://undraw.co

Combine: From zero to… Oh! I get it.(Part I)

Andrés Pesate
Devjam
Published in
9 min readSep 15, 2020

--

Often in life when we are presented with something new to us, that we don’t fully understand, we tend to treat it as a threat. It’s only after spending some time with it, making sense of what it means to us, that we start to feel comfortable around it.

In the IT world, this is no different. Every now and then we are presented with a new tool that creates a big fuzz in the community and everyone starts talking about it. The new cool kid in town. Nonetheless, this new cool kid, instead of encouraging us to get closer, often repels us.

The new cool kid I’m talking about today arrived a year ago (2019) with the name of Combine.

If you are like me and you didn’t have any experience with a reactive programming framework before, then you probably felt a bit like I described above.

That’s why I decided to write this series because now that I finally got to know it I can see the amazing potential it has, and I want to try to help you pass that initial intimidating phase so you can assess for yourself if you want to make it part of your programming life or not.

So, allow me to explain shortly how this series is structured so you can jump to the parts that interest you the most.

  • First, I will go over a few relevant concepts that will help you understand what Combine is.
  • Then, I will present to you a more hands-on explanation of its different members and how to use them.
  • And last but not least, combine all of it (😜) in a simple app so you can see how everything fits together.

If we look at Apple’s documentation about Combine, the first sentence we will read is:

“The Combine framework provides a declarative Swift API for processing values over time…”

From this definition, two concepts that caught my attention. The first is “declarative API” and the second “processing values over time”. So let’s see what they mean before going further into the framework.

Declarative Programming

When you search for declarative programming on Google you will most definitely stump upon many different concepts and definitions, and yes, it can get confusing. However, the discussion is centered around Imperative vs Declarative, so to put it in simple terms:

“Imperative programming is how you do something, and declarative programming is describing what you want to achieve.”

An analogy in the English language for this could be:

Imperative: I’m going to walk to the burger joint at the corner and get myself a cheeseburger.

Declarative: I want a burger.

In the first, you are specifically saying how you are going to get that burger you are craving, while in the second, you are merely saying what you want.

Moving into a more technical example.

Imperative Code
Declarative Code

It might not be obvious, but the main difference between these two examples is that in the first we are specifically saying how we are filtering out the integers, while in the second we only specify what we want to achieve, without going into the details of how to do so. Even though you are providing the predicate to which compare the results, you don’t know how it will be used.

This is the heart of declarative programming; it allows you to abstract your code from the context on which is running to give you the possibility to use it in many different scenarios.

It’s important to mention:

“Many declarative approaches have some sort of underlying imperative abstraction.”

If you are interested in diving deeper into the subject be sure to check out these references:

Reactive Programming

Now, when we look at the second concept, processing values over time, we could relate it to the definition of Reactive Programming (RP)

“Declarative programming paradigm concerned with sequences of data elements made available over time (data streams) and the propagation of change.”

In simple terms, Reactive programming is the practice of programming with asynchronous data streams or event streams.

It has many similarities with the Observer Pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them of any state change. However, one of the benefits of RP is that it allows a much higher granularity than the observer pattern.

For now, this should be enough, once we start discussing Combine it will make more sense.

If you are interested in diving deeper into the subject be sure to check out these references:

The introduction to Reactive Programming you’ve been missing

One last topic I think is relevant to discuss before we dive into Combine is Functional Reactive Programming (FRP), which is no more than the combination of the Functional and Reactive paradigms.

Ok, so now that we got the basics covered, let’s go back to the topic that brought us here in the first place.

Combine

By putting all of these concepts together we can say in simple terms that Combine is Apple’s approach to a functional reactive programming framework.

Now, like any other framework, there are some core objects that allow you to interact with it, in this case, we are talking about: Publishers, Subscribers, and Operators.

Publishers

As we said before, with combine we can process values over time, and a publisher is the object responsible for delivering these values. When new data becomes available, a publisher emits an event indicating this to anything that might be listening to it.

Technically speaking, Publisher is defined as a protocol with two associated types: Output and Failure. They represent the type of data this object can emit into the stream and as you can imagine, the Output data type is emitted when everything is going as it should, but for those cases when things go wrong, the Failure type allows us to pass down an error.

Similarly, there are three different events this object can emit:

  • An Output event when new data is available.
  • A Completion event that indicates the object fulfilled its purpose and will stop emitting events.
  • A Failure event that provides an error of the associated type. This event has the same result as the Completion one, after emitting it the object is not allowed to publish any further events.

Subscriber

Since we are talking about reactive programming, when something happens, something else needs to react to it. In Combine, this something else is known as a Subscriber. This object is the one that receives the events emitted by the publishers and reacts to them however it sees fit.

As with the Publisher, Subscriber is defined as a protocol with two associated types: Input and Failure. The input corresponds to the type of data this object allows to receive and the failure the type of error.

Operator

An operator is a function that creates either a Publisher or a Subscriber to react upon the elements it receives. That means that it can either emit or receive events.

These are convenience functions that will allow us to work with the data in the stream, by chaining multiple operators before finally handing it out to the last subscriber that will consume the end result.

It might not be clear now what’s the purpose of it but bear with me for now and I promise it will make sense in just a moment.

The Play

Ok, now we know the three main characters in this play, but, how does the play look like?

Let’s distant ourselves for a second from the technical details and let’s imagine we work at a cookies store. In this store, we have two rooms: one where the kitchen is and a second where we serve our customers.

Now, in order to provide the best culinary experience to our clients, we only sell freshly made cookies, which means that every time a person enters the store and buys a cookie, the counter asks the kitchen to provide them.

When this happens the kitchen starts the production line to make the cookies that goes like this:

  • Get the cookie dough.
  • Shape it into a cookie.
  • Add some nice chocolate chips.
  • Put them into the oven until cooked.
  • Deliver them to the counter to be handed out to the customer.

Working in this store wouldn’t be good for our diet, but it sure gives us what we need to understand how Combine works.

Think it through, we said we had three main objects, a publisher responsible for emitting events, a subscriber that consumes those events, and operators that behave like both and allow us to work with the data. If we try to map these items into the different roles of our store we could say:

  • The person at the counter would be a subscriber.
  • The person that gets the dough would be our publisher.
  • Each person in the production line would be an operator.

You might be thinking that if we need a different employee for each step in the production line for this simple store, how are we making a profit? Well, the business details go a bit beyond the scope of today’s topic, but it for sure helps us understand how an operator behaves as both publisher and subscriber at the same time.

If we look at the production line we mentioned above and try to define them technically, we would end up with the following:

Cookie Store Production Line

NOTE: The operator behaves like both Publisher and Subscriber, that’s why in the interface we define the Input and Output types.

Operator<(Input, InputFailure), (Output, OutputFailure)>

I mentioned above that the operator will allow us to do some work with the data. So, in this example, we see how we were able to chain them in order to create a data stream that transformed the initial data on each step until it finally delivered the object the subscriber was expecting.

One more thing… Back-pressure

Something we see in the example above is that it’s the subscriber who starts the whole process, rather than the publisher. This might be confusing as we could expect it to be the other way around. Instead of the subscriber asking for data, the publisher delivering data for the subscriber to process.

Well, the reason for this is that Combine inverts the relationship between these objects in a concept defined as back-pressure. What this means is that the subscriber controls the data stream by informing the publisher about how much data it needs or it can handle at a specific moment.

In simpler terms, it’s not the kitchen employee telling the counter how many cookies they have in inventory. It’s the counter employee asking the kitchen for X amount of cookies.

Lifecycle

We saw the role of each actor in the play, but we still don’t know the script of the play, so let’s have a look at it.

Source: Raywenderlich Combine: Getting Started

In the image above we can see the lifecycle of the relationship between a publisher and a subscriber.

  1. A Subscriber is attached to a Publisher by calling .subscribe(_: Subscriber)
  2. The Publisher acknowledges the subscription by returning through receive(subscription: Subscription) a subscription to the Subscriber.
  3. At this point, the Subscriber informs the Publisher of how much data (X) it needs from it. This could be either a finite or infinite amount.
  4. The Publisher may then (as it has values) send X (or fewer) values using receive(_: Input). A Publisher should never send more than the demand requested.
  5. At some point, the Publisher may optionally send a completion event. receive(completion:). This completion event will be either a normal termination or a failure propagating an error with it as we mentioned above.

Memory Management

Because we want to be good Combine citizens, we need to pay attention to our memory footprint. Every time we connect a subscriber with a publisher a strong reference is born between them in order to keep the relationship alive. This is being controlled by yet another protocol called Cancellable.

An object conforming to this protocol is returned upon every successful subscription and for as long as this object remains in memory, the subscriber and the publisher will continue to talk to each other, but at the moment this object decides to cancel() it or it gets deallocated, then the relationship between our actors will cease to exist as well.

I will explain further how this object looks like and how to work with it on the next part of the series. For now, just keep in mind that it exists and that it will help us a lot with our memory management.

That was a lot of information. Take a moment to digest all of it and I’ll see you in the next part where I will show you how all of it fits together at a technical level.

--

--

Andrés Pesate
Devjam
Writer for

Venezuelan in Amsterdam, working in IT as a Mobile Developer Consultant. Deeply interest in people’s motivations and connections with life.