Compositional Initalization
Introduction
This proposal introduces an opt-in protocol, PropertyInitializable
, which provides two new init methods:
- failable init from a collections of extensible, typesafe keypath-value Property objects
- non-failable init to clone from another instance, mutating select properties
The name “compositional init” means that this proposal allows the state of an object or struct to be assembled compositionally from sets of properties (including another instance). Compositional initialization allows mutations to be encapsulated in a clear, type-safe way.
This proposal addresses the problems that motivated Matthew Johnson's excellent proposal, SE-0018, but in a different way. Hopefully compositional init can serve as the implementation of SE-0018, which unfortunately got tabled due lack of ABI impact.
I initially wrote this proposal in 2018 based on Swift 4. I have reviewed the changes and proposals since then and it does not seem like there has been anything that would make this proposal unnecessary, please correct me however if I missed something.
From that review of past proposals, I find that this proposal may also address the desires expressed in the Swift Evolution discussion thread "Record initialization and destructuring syntax". I do not see any follow-up proposal for that, so hopefully this might help with that too.
I have a mostly working implementation made in the Swift 5 Playground here.
Motivation
Immutability carries many benefits including thread safety, simplification of state management, and improved code comprehensibility. It’s what makes functional programming so functional, which helps create unit-testable code.
However, in Swift 5, the benefits of immutability include neither:
(1) - "ease of making a copy that differs from the original in a subset of its properties — without lots of boilerplate,” nor (more generally),
(2) - "initializing an object from a collection or set of collections of its properties in one line of code."
The desire for no boilerplate and greater flexibility when initializing and cloning immutable types motivated this proposal.
Use Cases
A growing movement exists to use immutable models to handle data responses from the web in Swift applications.
This proposal was inspired by mocking a web service in Swift, where we had to simulate the server’s responses by changing a few properties of an immutable data model object from a canonical example. However, in addition to satisfying that one need, I found that this style of initialization allows for many other useful patterns that simplify code and improve clarity.
Detail of the Motivation
Chris Lattner’s SE-0018 proposal notes that in current solutions for (1), “initialization scales with M x N complexity (M members, N initializers).” I call this the “boilerplate cost” — measured on the “big B” scale. Indeed, one advised workaround for (1) involves going from B(M*N) down to B(2M), where M is the number of different properties to be supported for mutation during cloning.
Compositional Init, on the other hand, provides B(0) for copying an immutable object while changing some arbitrary selection of its properties, and also provides a B(0) solution for problem (2).
How Other Languages Have Solved (1)
Other languages have good solutions for (1) that are similar to compositional init, but none seem to solve (2) the way CI can.
Some examples of static, typesafe, functional languages that allow the initialization of a new instance via cloning with property overrides:
- Haskell’s’ default values in records:
newRecord = fooDefault { quux = 42 }
makes a clone wherequux
is overridden but thebar
andbaz
properties are copied fromfooDefault
- OCaml’s functional updates:
let newRecord foo = { foo with quux = 42 };;
- A Successor-ML proposal for functional record update proposal suggests:
foo {defaults where quux=42}
- Elm features updating records:
newRecord = { foo | quux = 42 }
Dynamic languages have been slower to embrace immutability, but support for this concept is growing:
- An ECMAScript 7 proposal for object spread initializer suggests:
let fooWithOverrides = { quux: 42 , ...foo };
etc. - An ECMAScript 6 library for clone + mutate
Proposed solution
Compositional init solves both (1) and (2) by adding simple, clear, Swifty syntax for the initializing an immutable instance from a set of typesafe properties and an optional clone argument. Because this proposal is based purely upon Swift 4/5’s wonderful KeyPath and Mirror types, we get all the type safety and access restriction guarantees that they already carry.
Traditional memberwise init:
let fool: Foo = Foo(bar: “one”, baz: 1.0, quux: nil)
Compositional init cloning fool
and mutating its quux
property:
let food: Foo = Foo(clone: fool, mutating: \.quux => 42)
Compositional init failably initializing foom
from an array of properties:
let properties: [PartialProperty<Foo>] =
[
\.bar => “two”,
\.baz => 2.0,
\.quux => nil
]
let foom: Foo? = Foo(properties)
Compositional init failably initializing foom
from variadic property arguments:
let foom: Foo? = Foo(\.bar => “two”, \.baz => 2.0, \.quux => nil)
As a result of being based on WritableKeyPath<Root, Value>
, the declaration \Foo.bar => “two”
will fail to compile if the property Foo.bar
is any of the following:
- not accessible in the current scope
- not writable in the current scope
- not the same type as the value being paired with it
- non-existent
Detailed design
This proposal introduces the following protocols:
- AnyPropertyProtocol
- PartialPropertyProtocol
- PropertyProtocol
- PropertyInitializable
Accompanying these, we introduce implementations:
- AnyProperty
- PartialProperty
- Property<Root, Value>
This proposal introduces the “partially type-erasing lazy assignment operator” =>
, which returns a PartialProperty<Root, Value>
from a WritableKeyPath<Root,Value>
on the left side, and on the right, an @autoclosure @escaping
that can accept either:
- a
Value
object, or - a function returning
Value
that will be lazily executed only at init.
Source compatibility
Aside from any naming collisions (sorry), this proposal should have zero effect on source code compatibility.
Effect on ABI stability
The initial PR for this proposal should not impact ABI stability, as far as I can tell.
Effect on API resilience
Compositional init should play nice, but I will leave it to the experts.
Alternatives considered
One alternative is to simply avoid immutable “set once at init” style properties, and instead use var
for any properties you might need to change. The pattern is then to “immutable-ize” the root type by using let
at instantiation, as in:
struct Foo {
var bar: String
var baz: Double
var quux: Int?
}
let fool = Foo(bar: “one”, baz: 1.0, quux: nil) // hah! can’t change me now!
var food = fool
food.quux = 42
let foom = food // immutable once again.
That workaround is not great because there is nothing to prevent mistakes or abuses.
As well there was discussion on the aforementioned thread on destructuring, to which you may refer.
Discussion
I created a mostly working playground implementation of this under Swift 5, which can be found on github here.
There are some limitations in the way Swift 5 handles WritableKeyPath. They cannot be used to write to a property before it is initialized, sadly. If they could be, that would really improve the flexibility of how this can be used. As it stands, this limitation means that any variable which you want to be able to initialize from a keypath-value pair (a Property) must be a var
and must have a default value or an initializer that gives it one.
Ideally, WritableKeyPaths could be used to initialize let
variable or a priave(set) var
, since the type adopting PropertyInitializable inherits an init method that, it stands to reason, should have access to do so. However, the way the compiler decides if a keypath can be considered "Writable" does not consider which scope the keypath will actually be used in when the writing finally takes place, or when it will take place (e.g. at init time, at which point a let
variable could still be writable, technically speaking).
Any tips from the compiler gurus on how this could be enhanced would be appreciated. Perhaps we would need something like an "InitializableKeyPath" indicating that the variable can be set only at init time, by an init function within the type, using such a KeyPath. (Note: WritableKeyPath did work with an uninitialized private (set) var
in Swift 4, but not in Swift 5.)
However if WritableKeyPaths to let
variables cannot feasibly be used even at init time, then we could consider that since compositional init is opt-in at the type level anyway, we might as well consider it opt-in at the property level, too, by making it a var.
I look forward to everyone's thoughts on this. Thanks