Functional Core, Imperative Shell

As someone who’s worked in the industry for over 7 years now with many large organisations I notice common problems arising in many pieces of software that do not handle side-effects well.

What is a side-effect?

A side-effect is an assignment operation — it changes the global state of the application.

private var n = 1 // global state func plus(_ value: Int) -> Int {

n += value // assignment

return n

}

Why is this bad?

If I have a function f(x) which returns a result, I would expect this to pass the test XCTAssert(f(x), f(x)) .

However, if the function relies on mutating global state this test will fail. Much to the confusion of the developer reading that test. In the above example XCTAssert(plus(1), plus(1)) would always fail because it relies on the mutating variable n .

While this is a rather contrived example, you can imagine how this might become an issue with other pieces of state in your application, particularly when you build an entire application on top of these mutating pieces of state.

A function that creates side-effects is called an impure function.

Impure functions are:

Not composable — we need to perform operations in a particular order or we get strange race-conditions

Unpredictable — the result changes over time

Not thread-safe — we will require a lock to perform an assignment.

Ideally, we want to reduce uncontrolled mutation of this global state. We want pure functions.

Pure functions only rely on the parameters that are passed in, and always return the same output for a given input. There are no side-effects.

Pure functions are:

Composable — you can build small pieces of functionality and combine them like Lego.

Highly testable — given a particular input you can expect the same output, no matter how many times you call that function.

Reusable — you can copy and paste it to another project and it won’t complain about missing dependencies.

Thread-safe — no matter what thread they are run on they will return the same value.

Variables

As soon as we introduce var ’s into our code we have to deal with time. Since a variable can change over time it is subject to race-conditions and dead-locks.

In Swift we should try to minimise the amount of var ’s in our code, particularly if they live outside of a function. Ideally, we want to be dealing with let values or parameters in functions most of the time.

Thinking Functionally in an Imperative World

Swift is not a functional language, it is a hybrid language. This means we can have the benefits of functional programming to enforce immutability and predictability and the power to exit out into OOP-world to perform side-effects or handle user interaction.

Direction of Flow

Most common architectures are bi-directional such as MVVM ( M<->VM<->V ), MVP ( M<->P<->V ), MVC ( M<->VC ). VIPER is probably one of the most complicated, requiring a Wireframe in order to put all of the pieces together.

By allowing the flow to occur bi-directionally, the order of events can occur in any order, creating debugging nightmares later on due to race-conditions.

For example, in MVP I may call a function on the Presenter before the View has been set, this may lead to unexpected behaviour or crash depending on how the code is implemented.

In order to improve predictability, reduce complexity, and reduce coupling in our code we should aim to use a unidirectional architecture. There are many different ways of achieving this some more involved than others.