Skip to content

Diffuser

Jesper Sandström edited this page Nov 2, 2020 · 2 revisions

We want our UIs to be as simple as possible when writing applications. The UI should not know how to make network requests, read from databases, or make any decisions relevant to our application at all. They should simply be a visual representation of the current state of our application.

We represent the state of our application in a data-structure called the model. The UI-layer should take this model and render it to the screen. For example:

func render(model: Model) {
    view.text = model.text
}

Here, we set the text of some UILabel on screen to the value of a field in our model. However, rendering is generally very computationally expensive. Our model likely contains other fields, so we should really only be running this if the value of model.text changed.

The naive solution is to introduce an explicit cache:

var textCache: String?
func render(model: Model) {
    if textCache != model.text {
        view.text = model.text
        textCache = model.tex
    }
}

However, this introduces a significant amount of code, which very quickly becomes difficult to maintain, especially when specific views require multiple fields of the model. For example:

var aCache: String?
var bCache: String?
var cCache: String?
func render(model: Model) {
    if aCache != model.a && bCache != model.c && cCache != model.c {
        view.text = "\(model.a), \(model.b), \(model.c)"
        aCache = model.a
        bCache = model.b
        cCache = model.c
    }
}

Did you catch the mistake? Of course there are better ways of structuring this specific example, but either way, this approach does not scale well.

Diffuser

The Diffuser encapsulates the machinery of this caching pattern in a way which significantly more concise and easier to reason about.

The Essence of the Diffuser

The simplest building block we need is a function:

{ oldValue, newValue in
    if oldValue != newValue {
        // perform side effect with `newValue`
    }
}

In the Diffuser, we encapsulate this machinery in a function called into:

into { newValue in
    // Do something with `newValue`
}

Composition

On its own, into is not much better than the manual caching we were doing before. The true power comes from the ability to compose it.

We will need two compositional properties:

  1. We should be able to combine diffusers of the same type, i.e.: Diffuser<A> + Diffuser<A> + ... + Diffuser<A> = Diffuser<A> Notice that combining diffusers in this matter is equivalent to simply merging all the side effects into one function:
into { newValue in render1(newValue) } + 
into { newValue in render2(newValue) } + 
into { newValue in render3(newValue) }

Is the same as:

into { newValue in
    render1(newValue)
    render2(newValue)
    render3(newValue)
}

For our desired definition of +. For this reason, we can simply provide another version of our into function which takes a variable number of Diffusers. For readability, we call this intoAll:

intoAll(
    into(render1),
    into(render2),
    into(render3)
)
  1. We need to be able to combine Diffusers of different types. To do this we need a function to convert between these types. We call this function map (technically this is a contramap, we chose to call it map for stylistic reasons):
func map<A, B>(transform: (A) -> B, other: Diffuser<B>) -> Diffuser<A> { ... }

The map function is what lets you "zoom into" different parts of our model. Consider something like this where we "zoom into" the Model's header field and bind its title and subtitle fields:

let diffuser: Diffuser<Model> =
    .map({ model in model.header }, .intoAll(
        .map({ header in header.title }, .intoText(view.titleLabel)),
        .map({ header in header.subTitle }, .intoText(view.subtitleLabel))
    ))

Conclusion:

The two compositional properties are what give the Diffuser its power. They allow you to declaratively structure your side-effects in terms of the structure of your model, and package all of this into a stand-alone function.

intoWhen and map are the fundamental building blocks of the Diffuser. Everything else is only defined for convenience reasons and can be derived by combining these.

Clone this wiki locally