zoul
(Tomáš Znamenáček)
1
I’m designing a custom JSON enum type, something like this:
enum JSON: Equatable {
case string(String)
case number(Float)
case object([String:JSON])
case array([JSON])
case bool(Bool)
case null
}
Initially it was read-only, but now users of the type are requesting writable subscripts and other mutation features. I’m not sure if I should offer only immutable API such as updating(key:to:) -> JSON or removing(key:) -> JSON, or if I should allow in-place mutation. In other languages I would go for the immutable API, but Swift already offers nice opt-in immutability with let vs. var. So should I simply offer a mutable API and let/var users decide? How do people approach this?
You could offer both, similar to how we have shuffle vs shuffled.
zoul
(Tomáš Znamenáček)
3
Doesn’t it feel strange to have a part of the API where all the functions just create a copy of self, apply a mutating method and return it?
1 Like
nrith
(Jason R Tibbetts)
4
The only thing I find strange about it is that I can't always remember which one is the mutating version!
Both uses (mutating and copying) are valid from the client’s perspective.
If you only provide the mutating operation yourself, copies require boilerplate:
let copy = original
copy.mutate()
doSomething(with: copy)
If you only provide the copying operation yourself, mutating requires less boilerplate but superfluous memory allocation:
original = original.mutated()
The best thing to do is implement a mutating method and provide a copying method that forwards to it:
mutating func mutate() {
// ...
}
func mutated() -> Self {
var copy = self
copy.mutate()
return copy
}
The forwarding methods can be somewhat abbreviated using a pattern like this, which also prevents the following common, easy‐to‐miss typo:
func mutated() -> Self {
var copy = self
copy.mutate()
return self
}
The API Design Guidelines recommend using:
- infinitives for mutating operations:
mutate(), and
- participles for copying operations:
mutated() (or mutating(argument) if it requires a direct object—appending(x) is less awkward than appended(x)).
- Express it as a computed property if it is O(1) and has no arguments; express it as a function if it is more complex. (e.g.
Collection’s first vs sorted())
5 Likes
zoul
(Tomáš Znamenáček)
6
Thank you, in the end I decided to offer both options. (Would it be interesting to have the option to get the non-mutating versions for free? Where something like @someAnnotation mutating shuffle() would also generate a shuffling counterpart that would copy self and apply the mutating method.)
DeFrenZ
(Davide De Franceschi)
7
And if you have something like the much-talked with you can do the copying version from the mutating one without writing the boilerplate (or at least the boilerplate becomes calling the function)
2 Likes
zoul
(Tomáš Znamenáček)
8
That’s what I did. I think it’s a good argument for having the function in the standard library.
nrith
(Jason R Tibbetts)
9
Thank you! I really do know which is which, but I always have to think for 1.5 seconds before selecting the one I want in the typeahead popup.