How to focus on your app’s syntax so it aligns with your app’s semantics
In this article, I want to explain how we can write fully declarative Swift. This coding style emerged naturally when I started working on an architecture I had designed to be easily adaptable in agile teams. Actually, the goal was to unlock agility for teams in the first place.
I will focus on the syntax and show how it aligns with the app’s semantics.
I will not show every facet of my architecture (Khipu), but you will find links to several articles I wrote about it at the end. One thing I want to mention is that it is fractal, meaning it uses (in theory: infinite) hierarchies. This means each level uses the same patterns as the others. And this article will contain some of those patterns.
But first, I would like to clarify some terms I will use.
Object Orientation: I want to define this term as I believe we do not generally share a common understanding of what OO is. For me, and in the context of this article, OO is defined by its usage of messages being sent between objects. Objects can receive and send messages at any time (n to m), which sets it apart from FP, where we have a strict 1-to-1 relation for a function being called and returned. As I will explain more in-depth later, I do not limit my objects to a certain type, like class and struct.
Functional Programming: FP has become a more popular contestant for OO over the past years. It is called such as it uses the mathematical idea of a function (pure, state/context-less) as its main building block. You will see that I use a combination of functions (partially applied functions) to create our modules interfaces, yet I want to emphasise that this isn’t FP. I actually use these functions to create and capsulate module state and functionality and also to communicate in an OO fashion. Though I have taken many ideas from FP, such as immutable data types and immutable states.
Immutability: Without going into too much detail, we should always strive to create code that is as immutable as possible. Why? Well, I would refer you to your chosen search engine as many important discussions are ongoing. But to keep it short: types and data that cannot change also can’t change unintentionally — a whole category of crashes and bugs are eliminated.
Module: In this context, a module is a scope-defining element with a maximum of one input and output. Modules understand either an app-wide message type and use it for their input and output, or they offer their own types, like Request/Response. I’ll use these when I get to them later in the article. In any case, I call those types the Module’s DSL (Domain-specific Language).
Usually, the go-to type would be a class. I will try to explain why I consider other types better suited for the job of a module than those.
Declarative vs Imperative
Now, let’s look at some canonical examples, starting with the following:
sumImperative is created in an imperative style by explaining the task step by step. sumDeclarative0 is created declaratively by creating an array of values from 1 to 100, which are then reduced. To reduce them, we provide a starting value (0) and a simple additions function (result, newValue in result + newValue), where the result is either the starting value or the result from previous calls of the provided function. As the provided function is only adding two ints, we can pass in the + operator as a function instead, as seen for sumDeclarative1.
I argue this style is more natural, as humans do not repeatedly tell each other the steps to do certain things.
If I needed someone to sum up all numbers in a list, I would not ask them: “Please type the first row’s number into the calculator, press ‘+,’ now repeat with the next row and continue until you run out of them. Then give me that number”. No. I’d say, “Please, sum up all numbers in that list and let me know the answer.” And once I got used to the FP’s nomenclature, I find
expresses that better than
- Imperative programming: Telling the computer how to do something.
- Declarative programming: Telling the computer what to do while using math lingo.
This is the point where most articles on that topic end. They usually show you how to use declarative programming on the method/algorithm level.
But I want to share a declarative coding style applicable at higher levels. It requires some out-of-the-box thinking.
Real-world snippet from an XMMP chat app:
This code lives in the user interface and is executed once the send button is tapped. roothandler is a function/closure callback, which takes a Message parameter. Message is a nested enum with annotated model structs. The message in this example is Message.chat(.send(…)) with a ChatMessage struct as an associated value.
ChatMessage itself is an immutable struct. Necessary changes — like appending an image selected in UI — are reflected by calling alter and passing in a Change enum (similar to a message) that will generate a new chat message reflecting the changes.
Let’s start with bisecting ChatMessage
ChatMessage.Change defines all possible interactions. In this case, there are the following:
- .wasReadBefore(Bool) indicates the User might have read this message.
- .add(.image(ChatImage?)) appends an image to the list of images, as we can see in the second case in alter(…). I allow an optional here, as this will reduce the code paths in the UI.
- .remove(.image(ChatImage)) filters the image from the existing list, as seen in case three in alter(…).
Immutable AppState
The ChatMessage from above is a model type, representing a tiny part of the app state at a certain moment.
We can organise the whole AppState in an immutable struct, very similar to ChatMessage itself. Here’s the code:
Adding a new chat to the app’s state would look like the following:
This AppState.Change DSL is the simplest one possible, as it exposes the data fields the user can change. The downside is that we would write a very clunky term each time we want to add a simple data item.
But we can write the DSL as we like it, so it is easy to adapt it to allow this:
or
Definitively more declarative than with the first DSL. I encourage you to go crazy and try things out, as the DSL is pretty easy to replace. And as it is defined as types, the compiler will tell you where to change stuff. How those more complex DSLs are designed is shown in the ChatMessage code.
Storing the state
Since AppState is immutable, it will be recreated each time a change needs to be reflected. Therefore, we need an object that wraps the state and keeps the newest version — a Store.
But here, I don’t want to use a class, as it offers me more than I need. I would classify this as accidental complexity or “where the bugs live.”
As we will later see, we could use a combination of three methods to create a reference type that we could use as the app store’s store, but it would pose quite some challenge to request something from it. Either we would have to hand over callback, which would clutter up the code, or we would have to use some form of builder pattern and create an accessor function each time we want to access its values via its returning.
Instead, I use a tuple of functions. The tuple itself is a value type, but since all tuple members will be functions (reference type), this doesn’t pose a problem.
Access returns the current state.
Change accepts AppState.Change values and forwards them to the state.
Reset resets the state with a fresh AppState object. Callback implements a simple notification pattern. The store is used like:
Listening for store changes is as easy as:
change : { state = state.alter($0) … and reset : { state = AppState() are the only places outside of the UI that require mutability.
The Message type
Message is an app-wide type. It is used by the UI and the app’s features, a kind of module that listens for every message and will react to those it is interested in.
Let’s look at some examples.
To log into the service, the UI might trigger the following:
If the user taps on a button to subscribe to another user, it might look like this:
In UIKit
In SwiftUI
Our earlier example
I find this example especially intriguing, as in a single statement, we do several things. But most of all, it comes quite close to natural language:
“Chat, send chat message from user to receiver with text as body after adding the selected image.” Add another “Please,” and it would be what I would say if the chat feature was a human being instead.
Now that we have seen a model type, the message, and how to use those together, let’s inspect how to handle those messages.
Features and usecases
The Message type we have seen above is shared among all features. Again, I have decided to neither use classes nor structs as module types because they offer too much stuff I don’t need.
Instead, I use the following construct:
createTodoListFeature is a partially applied function. It is not used often in OO, even though they are super versatile. You will find articles on the Internet describing how partially applied functions can replace almost all OO-pattern in FP. But this is wrong: partially applied functions violate the purity that FP requires from their functions.
But what is happening here?
createTodoListFeature is a function that takes another function as a parameter — the output — and returns an instantiated but not executed function.
This function is an Input type. We can assign this function to a variable or put it into an array and pass in every Message we receive. We can say that the Input function itself is the feature.
In createTodoListFeature’s body, we instantiate the UseCases this feature uses.
The UseCases are modules that work a little differently than those we have seen before. I have taken those UseCases from Robert C. Martins’ “Clean Architecture.” You will find a lot of information about them on the internet.
UseCases are Protocol and Associated Types (PATs)
and are implemented like this:
A UseCase is meant to do one thing. Therefore, its Request type often will just have one case. The Response type often will have more than one case, for success, failure, or in scenarios where different things might be returned.
Why do I use PATs here? because they allow me to bundle the use case with its request and response. Therefore, I chose a struct here, even though it offers more than I need.
Note that the Interactor itself is private. We achieved perfect black boxing. And it is a class, so you might apply any OO style you prefer here. Also, this ensures we can connect to libraries and APIs. Robert C. Martin uses gateways in his clean architecture’s use cases. I want to point out that the store is a gateway in this example.
Assemble everything and connect the UI
I call all features together the AppCore. Let’s see how it is created:
createAppCore works like the create<Feature> function. It returns an Input function that will only iterate over all receivers and features and forward each message. This function is used like this:
ViewState is a receiver (and will be informed before any feature) and an ObservableObject. As such, it is accessible in SwiftUI.
In UIKit, I found the following construct to do the job:
Now you subclass each ViewController from ViewStateViewController and if a new state is available, handle(changed viewState: ViewState) will be triggered.
We completed the circle. We started by sending Messages from the UI. Now, we are updating it for any state change.
At this point, I want to link to Primitivo, a framework that allows declarative programming in UIKit.
So much more to say…
OK, that was a wild ride. I tried to keep this article brief yet clear. I don’t know if I managed to do that.
- Unidirectional flow
- Fully decoupled UI
- The AppCore doesn’t change during runtime
- Super easy and fun to test
These are all highlights I couldn’t elaborate on in this article. Some of these topics you’ll find are covered in the following resources:
Resources
- Using Swift’s Types as Domain-Specific Languages
- The Ultimate Domain Language: Declarative Swift
- Axiomatic and Brain-sized Coding in Swift for Creating Systems of any Scale
- Domain-Language-Based Systems in Swift
- Using Swift’s Type System To Model Behaviour
- Introducing Khipu: My Boilerplate Code-Free Implementation of Clean Architecture in Swift
- Diplomacy in Declarative Swift
- Khipu: Rapid and Sustainable Software Creation in Swift Through Declarative Domain Paradigm
- Architecture & Agility: If It Doesn’t Feel Agile, It Ain’t Agile
- A Lighting app
- A Snake app
- Many more examples
Fully Declarative Swift for Real-World Projects was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.