Declarative Swift in Action

Declarative, Immutable and Brain-sized Swift

Manuel Meyer
DevGauge
Published in
8 min readOct 19, 2021

--

or: Did I Discover a New Paradigm?

«Any fool can write code that a computer can understand. Good programmers write code that humans can understand» — Martin Fowler

I want to introduce you to a code organization style that I discovered and I believe constitutes a new paradigm. The goal of its design is to write smaller, leaner and more maintainable codes that fit in anybodies brains.

Partial Application for Modules

Every architecture in Swift that I came across uses classes as central elements, which is the goto-type for reference types. But classes offer a bunch of interactions. They allow any number of methods, they are subclassable and mutable. And while these are all things I don't need for modules, the one thing I need them to be able to do, classes do pretty badly: They have been advertised as the perfect tool for black boxing for decades, though they offer gray boxing at best, which is proven by the fact that one often requires extra subclassing information beyond the class’ signatures. Classes fail at their core promises. Also, they are actually incapable of providing a flow-through themselves — implementing this requires pattern magic. Actually, I would join Ilya Suzdalnitski and argue that by far most patterns are only necessary to deal with a class’s shortcomings:

There’s no objective and open evidence that OOP is better than plain procedural programming.

The bitter truth is that OOP fails at the only task it was intended to address. It looks good on paper — we have clean hierarchies of animals, dogs, humans, etc. However, it falls flat once the complexity of the application starts increasing. Instead of reducing complexity, it encourages promiscuous sharing of mutable state and introduces additional complexity with its numerous design patterns. OOP makes common development practices, like refactoring and testing, needlessly hard.

Classes having many different forms of interaction and manipulation leads to a higher complexity than needed. This will inevitably lead to faster code deterioration and slow down any team to zero speed.

Any architecture centered around classes has the potential to fail horribly. Today it is widely accepted that Apple’s MVC is flawed, but in my observations improvements like MVVM, VIPER and VIP only slow down this deterioration for a while, but in the end, they have the same development speed issues as if they had no particular design at all. In a way, classes act as entropy amplifiers, leading to disorder.

Also in most implementations, a core characteristic of architecture is violated: Often they are not decoupled from the UI. Rather they spend many resources on UI integration when the architecture shouldn’t deal with this question in the first place.

Instead of classes we want something that truly works as a black box and offers exactly one code execution path as this gives us the least complexity possible — we need a structure with one input and one output.

We don't want classes at the center of our design, instead, we use a technique that predates OO by decades: Partiality Applied Functions.

Partially applied functions are functions that, when executed, return another function which then can be written to a variable and called over and over again at will.

createAdder keeps the state in the variable value and returns the function that will add to the value and return it. Note, that we will be handling dependencies in the same way.

We can also use partial application to return a tuple of functions, i.e. to create a stack:

Domain-Specific Language encoding

Now this partially applied function only takes one parameter, but if we want it to behave as a module we want it to understand different messages. We achieve this by typing the parameter with a type Message which is a nested enum, wrapping model types as needed.

The following messages can be used, as seen in the executable documentation, which lists examples for all possible “Vocabulary ”:

We will use this DSL approach everywhere

  • Features will communicate via the Message type
  • Features are broken up in UseCases, which each use their own DSLs
  • The model types have their ownChange-DSLs
  • and the state is also managed through a DSL

DSL Examples:

  • Message to request switching to slider UI:
  • Following is an example of a new light model object being created and changed through it’s Change DSL:
  • requesting UseCase LightsLoader to load lights and rooms:
  • updating lights in store

That’s it. Partial Application and DSL are pretty much all we need. Let me show you how we can create lean code efficiently by using them.

All the code is declarative rather than imperative. It describes a program in terms of What you want it to do, not How, leaving the compiler to fill in the gaps.

Immutable Models and State

First, let’s dive into the immutable models and state.

Why do we want immutability? There are actually several reasons, but first of all: If something can’t change it also can’t change accidentally. A whole class of errors and crashes just can’t exist when models are immutable.

Mutability also increases complexity, which we don’t want. While Immutability helps us to create reasonable code: Code, that is predictable.

Let’s look at the Light model:

(doesn’t it look like Elvis Presley?)

Light is fully immutable as it only has let declarations. To change it, we need to recreate it with the changed value overriding the previous one. All possible changes are defined in the Light.Change enum and they are processed by handing them to alter, resulting in:

The state is implemented very similarly:

which is used as

As we recreate the AppState for each change in the system, we need a place to store and access it.

Store is a tuple of functions to access, change, reset and destroy the store. It also includes a callback mechanism to implement an observer pattern.

We use partial application to instantiate a store:

The store’s state is changed by forwarding AppState.Change values

Here, the old version of light is replaced with a new one, identified by its id.

Features & UseCases

Now that we have seen how state and store are done, let us have a look at the next layer.

The lighting app contains a LightingFeature. Again, we use partial application:

A feature consists of UseCases. They are taken from Robert C. Martin’s “Clean Architecture” and can be found in other architectures, i.e. VIP.
Here we have the usecases loader, switcher, valueSetter, dimmer and changer. A Feature accepts a message value and translates it into the use case’s DSLs as needed. Once a UseCase finishes a task, it will call back and the process and handle functions are called. A new feature message might be handed back to the app via the output function, which is the roothandler passed in during app creation.

UseCases are PATs — Protocol & Associated Types

The use case Dimmer increases and decreases different light values.

UseCases contain the logic implementation and will be instantiated with all dependencies, in this case HueStack and Store. Features and UseCases are again immutable, only Store actually contains mutable calls, writing the state and registering observers.

Assembling the App

We have seen how we create UseCases and how they are used in features.

Now we need to combine all features and formulate the AppCore.

The only task this AppCore needs to do is to accept messages from features and forward them to all features, after forwarding them to receivers. Receivers are similar to features but passed in from outside. We use this to connect the UI:

App Root

Note, that the roothandler is injected into itself as roothandler. This means that the features and the UI callback directly to the app root.

ViewState

I will not show all UI code here, they are pretty conventional, you will find a link to the git repository at the end, just a short SwiftUI snippet:

Now this it is, the app is complete.

Brighter Hue Interface

What we have seen here I call “Core|UI” (project name, subject to change),

In Core|UI every message flows through the system exactly on the same path

Message Flow
  1. A UI interaction or some other event is triggered and passed into the AppCore (just “App” in the diagram). The Message is forwarded to each feature.
  2. Pattern matching determines which use cases will be requested
  3. A use case finishes its task and calls the parent feature.
  4. The feature forwards a request to another use case. This is rare but possible.
  5. That use case calls back and the feature determines it has finished and forwards the result in an appropriate message back to the AppCore — the circle begins again.

Core|UI is a general application programming architecture, any app you can think of can be described in it, and here is proof that the subset of Swift functionality is fully capable:

Declarative Turing Machine

In this part, I will introduce you to a declarative Turing Machine. The fact that I am able to create Turing Machines proves that the declarative toolkit I have presented here is Turing-complete — the subset is as powerful as the complete Swift language:

Here the tape implements the state instead of a store, therefore the tape does manage mutable variables. Example machines:

Turing Machine Examples

Core|UI and the Turing machine offer very different experiences, but only to the programmer. From the compiler’s perspective, they are very similar: a tree of partially applied functions processing DSL values. We could inject a store into the tapes which would make the characteristics of the Turing machine the same as code written in Core|UI, but I decided against it, as a machine where the tapes keep the state is closer to the machine described by Turing himself.

Another strength of this declarative style is the easy testability. I use Quick and Nimble in the following tests. Every it("…"){expect(…)} is one test. They are defined in a cascading style, but each test is executed in isolation, for every test the cascade is applied again.

In this spec 45 lines contain 33 tests. The tests are very small and focused, more like describing logical rules. As all elements are interfaced by just one function, UI code is actually identical to the test calls.

In the beginning, we saw that the DSLs allow for using code as documentation, as we can write down every command that can exist. But as this is code, it can also be tested, as we can see in this lighting feature tests, lines 24 to 36.

A Paradigm?

Finally let's answer the title’s question: Is this a new paradigm?

In this context, a paradigm is a system for creating distinct architectures. A paradigm is defined by the constraints it introduces to the code which again serves as guiding lines for the coder.

  • Structured Programming introduced the concept that the executions path needs to be well-defined (“goto considered harmful”)
  • Both FP and OO try to solve the same problem: mutable global state, FP by removing mutability, OO by hiding mutable state

Even though the architectures I have shown here are very different in the eye of the user, they have certain things in common:

  • The module building type are functions
  • Behavior is encoded in type-based DSLs, decoded via pattern matching
  • Mutability is confined to one or very few places, exclusively in local function scopes

As this system is defined by those constraints, I argue that this is a paradigm, the Declarative Domain Paradigm.

A presentation I gave earlier this year:

The light app code is shown here: Brighter Hue

The Turing machine: DeclarativeTuringMachine

An article using declarative swift to implement a board game.

Example todo app from the presentation: Todos Core|UI

Feel free to ask any question, in the comments here or via tickets on GitLab.

--

--

Manuel Meyer
DevGauge

Freelance Software Developer and Code Strategist, currently working on a book about the Declarative Domain Paradigm. Want to publish me? Get in contact!