-
Notifications
You must be signed in to change notification settings - Fork 42
Value Semantics
Models, Events and Effects in Mobius.swift are required to have value semantics. A type has value semantics – as opposed to reference semantics – if modifying a local variable of that type can’t cause changes that are observable elsewhere.
It’s easiest to understand this through counterexamples. Class types do not, in general, have value semantics, because a local variable in one place may refer to the same object as a variable stored somewhere else, and a change through the local variable may be observed through the “somewhere else”. However, if a class is immutable (nothing in it can be changed after it is initialized), it can be considered a value.
Closures can be considered values if they are pure. A function that changes a global variable, or an object of class type, or has side effects, is not a value and should not be included in Models, Events or Effects.
Structs, enums and tuples are “value types” and have value semantics if all their properties/associated values have value semantics, and they don’t have impure methods or accessors. Standard library collections with copy-on-write behaviour almost have value semantics, and are “close enough” for Mobius’s purposes.
The concepts in Mobius have their roots in functional programming languages like Elm and Haskell, where all values are immutable – they can’t be changed after being created. Some similar libraries in languages with mutable types demand the use of immutable types. This includes the Java version of Mobius, because Java doesn’t support user-defined types with value semantics.
Swift’s type system allows us to be somewhat more flexible: Models, Events and Effects can be mutable value types because the language ensures that values held my Mobius can’t be mutated “behind our back”. For example, even if the Model of a Mobius loop is a struct with var
properties, you can’t change the copy of the struct managed by Mobius except through the Update and Initiate functions.
Nevertheless, there is an argument to be made for only using immutable types – enums, tuples and structs with only let
properties. Doing this makes it impossible to modify a single model in multiple places within your update function, and guides you towards a pattern where modified fields are collected and then applied by constructing a single new Model immediately before returning. Proponents of this approach argue that this makes it easier to understand your Update function, since you can’t miss a model change.
The downside of this approach is that creating new models with many fields, where only a few are changed in response to a given event, results in error-prone boilerplate that often gets copy-pasted. Helper methods can improve this situation, but still require boilerplate (or code generation).
One middle path is to allow Models to be structs with var
types, but only modify them once. The Copyable
protocol in MobiusExtras can help with this:
struct MyModel: Copyable {
var label: String
var value: Int
}
func update(model: MyModel, event: MyEvent) -> Next<MyModel, MyEffect> {
// Note that model, being an argument, is immutable
switch event {
case .increment:
return .next(model.copy {
// $0 is an inout MyModel, which starts as a copy of `model`
$0.value += 1
})
...
}
}
While Events and Effects can be mutable, as long as they have value semantics, we have never seen a case where this is useful (except in Mobius’s own unit tests, where we mostly use String
s for convenience). In real-world loops, Events and Effects are usually enums.
Getting Started
Reference Guide
Patterns