Keyword for immutable instances

I like the idea of having a section of a class that is all-let, all-immutable e.g.

dual class Matrix, MutableMatrix { 
  immutable {
     let ivar : [Float]
     init () { ivar = [0, 0, 1, 0, 1, 0, 1, 0, 0] }
  }
  mutable {
     var ivar: [Float]
     func setNthValue(_ n:Int, _ value:Float)
     func multiplyIntoSelf(matrix:Matrix)
  }
  common {
     func multiplyBy(matrix:Matrix) -> Matrix {}         
  }
}

Programming is about contracts. If someone says "This object is immutable" and you find a way to reach into it and alter it, then any hacker can do the same, and who knows perhaps leverage that into an exploit.
There has to be an insurmountable barrier between mutable and immutable stuff lest credibility breaks down.
If you want to make a copy of something and you're allowed to do so, then great, but to reach into a fixed object and change it should be blocked. Otherwise might as well be programming in C.

I think what I and some others in this thread are still having trouble understanding is: what can you do with your dual class that you can't already achieve with

struct Matrix {
    private var ivar: [Float] = [0,0,1,0,1,0,1,0,0]
    mutating func setNthValue(_ n: Int, _ value: Float) { ... }
    mutating func multiplyIntoSelf(matrix: Matrix) { ... }
    func multiply(matrix: Matrix) -> Matrix { ... }
}

The contracts you're looking for are already enforced for structs — what I'm trying to understand is, what is the differentiating factor that you're looking for between using a struct (which has exactly these semantics) and a class (which purposefully does not)? In what ways does a struct not solve the use-case you're already looking at? A clear list of the differences you're looking for will really help understanding and motivating any change in this area.

5 Likes

Retain and release. Let's say the matrix is 9000 elements in size, or 9 million. What then?

Can you elaborate on that? Retains and releases are not an end unto themselves. I'm not exactly sure how that applies to the matrix case, regardless of size.

1 Like

Well OK it's two things. First memory management by retain/release logic, so that objects can be shared and not constantly be duplicated.
Also code legibility is enhanced by the Mutable- prefix.

You're trying to bring Obj-C kind of contract to Swift, which uses a different kind of contract for this very same functionality. If anything, I think a lot of us are confused by that.

2 Likes

For what it's worth, Swift is pretty smart about structs. They're not duplicated unless they need to be, and large-enough structs are heap-allocated, allowing free multiple references to the same struct. Structs are only copied when mutated, and only when there are outstanding references to those structs elsewhere.

In any case, even in the matrix case, copying is relatively cheap: the real weight of storage in Matrix is the buffer of ivar, which is heap-allocated and hidden behind a value-type Array. That array can be copied very cheaply until mutation because it doesn't make a copy of the backing buffer every time.

2 Likes

A third benefit is that classes have inheritance.

FWIW, most CoW types duplicates data only when they mutate and the data is shared with another variable. If anything, I don't think it can get any more minimal than that.

:thinking: sure, I'd be more on board with bringing inheritance to struct instead. Though I can't even begin to fathom how that'd work.

Given the amount of type inference we utilize, reliance on type names might actually be pretty bad.

Actually, I take that back. It doesn't look similar to Obj-C usage that I know either.

1 Like

You folks are making good points but in the end, I do worry about really large objects where mutation will be frequent. Using too much RAM on iOS can result in an app getting killed. So if the object is let's say 10 megabytes, and the plan is to mutate it 10 times a second, while passing it around, then I wonder if it would cause the app to get killed.
Plus there is the inheritance issue, and how adding inheritance to structs may be infeasible.

That's a good point, but I think your design(?) also suffer from the same problem, if not more. You put data into a fixed state, now you need to make a copy if you're mutating it. If anything, it would be much more defensive (and wasteful) than CoW. And if you misuse the type, without compile-time checking, you'll just straight up crashing.

2 Likes

To expand on what @lantua says — in order for this to be a problem, you would have to be constantly mutating structs while still holding on to all of the old copies. This is a pretty unusual situation to fall into, even accidentally, but the key is that in order to safely mutate objects the way you're suggesting, you'd need to copy them around anyway (since if you fixed them, you'd get an exception on the mutation). In that case, you'd be no better off than you are with structs, except with additional code to make the copies yourself.

2 Likes

I guess I need to know how the CoW works. How much of a struct needs to change in order to trigger the copying of the entire object?
If I have an array of Int that is 10000 Ints in size, and I change each element from 0 to 9999, one by one, does the array get copied 10000 times or one time?

Hi @oof,

It looks like you may not yet be familiar with some common design patterns in Swift. This language has both value types (for example, structs and enums) and reference types (classes).

In general, you would use a reference type when what you're modeling has a notion of identity. In the case of a matrix, you would model this using a value type because any two matrices with identical values are completely interchangeable. So, such a type would be written idiomatically like this:

struct Matrix {
  var storage: [Float]
}

Notice that storage is declared using var. There is no need to create a pair of mutable and immutable types, since you can create both mutable and immutable values of type Matrix:

let x = Matrix(...) // immutable
var y = Matrix(...) // mutable

Since y is declared using var, y.storage is mutable; since x is declared using let, you can't mutate x.storage even though storage itself is declared using var.

By contrast, if I'd written let storage: [Float], then both x.storage and y.storage would be immutable. There are uses for such a design, but in general you should think twice if you're about to do it.

You do not need to worry about the efficiency of using value types everywhere; there are advanced techniques that the standard library and compiler will use to make sure that things work correctly and efficiently for the most part.

At some point, you may get to the point where you'll need to manually fine-tune complicated value types for performance. At that time, you can study how the standard library uses copy-on-write to provide value type semantics without actually eagerly making copies. It is an interesting technique and not too complicated once you're familiar with the basics of Swift value and reference types, but definitely not as beginner-friendly as it could be.

(There have been thoughts as to providing a @cow (COW being the abbreviation for copy-on-write) annotation so that users can implement their own types with this design pattern more easily.)


Incidentally, we use an "ed/ing" rule in naming APIs for Swift. This also goes to the subject about mutating instances:

Given a pair of functions, one that mutates in-place and another that returns a result, the mutating function is named using a verb and the non-mutating function is named using a noun, typically formed by adding "-ed" or "-ing" to the verb. For example, in Swift, we would write mutating func multiply(by other: Self) and func multiplied(by other: Self) -> Self. Swift users know to look for mutating/non-mutating functions by their names in this way. (There are some exceptions to this rule, which you will encounter if you haven't already.)

Finally, you'll notice that Swift doesn't typically use functions named getFoo and setFoo. Again, this goes to how values can be mutated.

You can create computed properties with custom getters and setters, which can be used by your users as though they're stored properties in many ways:

var foo: Float {
  get { return storage.count > 0 ? storage[0] : 0 }
  set { if storage.count > 0 { storage[0] = newValue } }
}

In this example, users of my type could write x.foo = y.foo, instead of x.setFoo(y.getFoo()) as in some other languages.

Getters are automatically assumed to be non-mutating (as functions are), while setters are automatically assumed to be mutating, since most setters, well, mutate. You can write a getter that mutates a value by declaring mutating get. It's sometimes overlooked by users that, if you'd like to write a non-mutating setter, you can declare nonmutating set.

In a similar vein, you can create subscripts (where the indices can even be of a custom type) in order to access elements of your own custom collection type using square brackets ([]), just like Array does in the standard library. If you're creating a 2D matrix type, for instance, you could even take a pair of indices for your subscript:

subscript(row: Int, column: Int) -> Float { ... }

Even though the syntax makes a subscript look as though it only returns a value, you can write both a getter and a setter for a subscript, just as you can for a computed property. The setter, naturally, is also mutating unless you specify otherwise.

Now, as you wrestle with more advanced types, you may run into some efficiency issues with value semantics and subscripts. It is for that reason that Array, for example, uses an unsupported feature called _modify, which is not yet ready for public consumption. Hopefully, by Swift 6 or 7, this feature will be mature enough to go through Swift Evolution so that all types can be as efficient as Array when it comes to accessing subscript elements for in-place mutation.

Hope this helps to explain how you can use idiomatic Swift to create custom types which have the behaviors that you're looking for.

10 Likes

That depends on the struct, but usually it's any mutation. It's all implemented by hand in the standard library, so you can get it as granular as you need it to be. This B-Tree library is a lot more granular and only copies at the node level. You don't want to get too granular though, because you'd need a separate allocation for each set of things that can be copied.

1 Like

If you are mutating the same array in-place, then at most it is copied once (if there is another instance of the array using the same backing buffer on the first mutation). Once the buffer is unique to your array, you can mutate it any number of times; the idea behind copy-on-write is that unique references require no copies.

Subtyping relationships are something that may or may not eventually come to value types, but Swift offers protocol-oriented programming (POP) for value and reference types alike. Protocols allow you to create complex hierarchies of API contracts without concrete types at every level of the hierarchy, and allow those contracts to be used in generic code.

5 Likes

That I can explain. Though there's a little nuance here, and it may not even make a copy at all. The reason being that if the array is unique, i.e., it's not sharing its storage with other arrays, then it'll do in-place mutation. If it's not unique, it'll first make a copy (all full 10000 ints, unfortunately), then mutate that copy.

So, assuming during first mutation, array shares storage with other arrays, that'll be 1 copy. In the subsequent mutations, though, if you don't assign the mutated array to other variables, it'd still be using the copied storage, which is now unique (not shared with others). So that'd be 1 copy in total. If array is unique since the beginning, it'd be 0 copy!

However, the caveat is that, if you assign array to a variable array1 after each mutation, and both are alive during the subsequent mutation, the storage are, at that moment, shared between two variables, and will not be unique. So mutating array will incur another copying, totaling 10000 copies if the array is unique in the beginning, and 10001 if not.

4 Likes

I've seen POP in the real world and it risks a real possibility of confusion, because in my experience programmers will scatter the protocol definitions across many files, and each will modify ivars without consideration of what other protocols are doing, leading to bugs. POP is not a panacea and I'm not sure it solves what it claims to solve. It just makes some testing easier. But I suspect POP is used in Swift not because it makes testing easier, but because structs don't have inheritance.

1 Like

That's interesting. Hopefully the compiler will warn of that second scenario.