Why are structs in Swift said to be immutable?

I've hear many times being said that

structs in Swift are immutable types

But it seems to me mutability has to do with with let and var, because you can easily mutate a struct instance like this:

struct Point {
      var x: Int
      var y: Int
}

var p = Point(x: 6, y: 7)
p.x = 4

So then, why is it said that structs are immutable?

Structs are copied on mutation. That is, the original is not mutated. This is in terms of observable effects. The compiler will optimize in-place mutations whenever it can.

5 Likes

Thanks Avi,

I see, so every time a struct is being "mutated" a totally new instance is actually created in the background?

Most of the time, no actual copy is made. Usually, the compiler can optimize out the copy and do an in-place mutation. But logically, you should write code as if that copy takes place every time.

5 Likes

Yep, you're right, structs are not immutable.

The thing about structs is that they are values. That means every variable is considered a copy, and its members are isolated from changes made to other variables.

Structs are not copied on mutation. They may be mutated in-place. But every variable is a new copy (including passing the struct value as a function parameter).

6 Likes

In functional languages values are truly immutable (so in order to change oven's temperature you need create a new oven in a new kitchen in a new house). In swift you can mutate oven's temperature "directly" (and the corresponding new kitchen and new house are "created" behind the scenes although you can consider that as a "modified" kitchen and modified house vs them being "new"). Struct / tuple values are only immutable if they are "let" vs "var". OTOH, enum values are more like true immutable values of functional languages - you can not change enum's associated value, you need a new value with a new associated value to do that.

Edit: having said that, swift would be more symmetrical if it either allowed enum's associated value modification (like in structs), or had truly immutable structs & tuples (so only "let" in structs).

Edit 2: example showing struct values are truly mutable:
struct Litmus {
    init() {
        print("Litmus init")
    }
}

struct Struct {
    init() {
        print("Struct init")
    }
    var litmus = Litmus()
    var int = Int()
}

func test() {
    print("struct test")
    var s = Struct()
    print("b4 struct mutation")
    s.int += 1
    print("after struct mutation")
    print("end")
    
    /*
     struct test
     Litmus init
     Struct init
     b4 struct mutation
     after struct mutation
     end
    */
}

test()

Note that between these two lines:

 b4 struct mutation
 after struct mutation

Neither Struct.init() not it's field's Litmus.init() was called.

I won't call this optimisation (optimisation is more like something that compiler can do when it can and not do when it can't, or do in release but not in debug, etc).

1 Like

I don't think this is a valid example. If the compiler created a copy of s, it would just copy the memory of s rather than call Struct.init() to create a brand new copy.

May I ask what's the benefit of thinking it in this way in practice? (I suppose what you said is general and not specific to COW).

I'm not sure, but I think some syntax in Swift seems to suggest what you said is the right on concept. For example, in the code below the setter in the protocol definition at line 6 seems to suggest when bar is changed at line 12, the compiler has an internal setter function to overwrite the original bar with a new value. I know the actual implementation is in-place modification, but on concept it looks like creating a new copy and overwriting the original copy.

 1	struct Bar {
 2	    var x: Int
 3	    var y: Int
 4	}
   
 5	protocol FooProtocol {
 6	    var bar: Bar { get set }
 7	}
   
 8	struct Foo: FooProtocol {
 9	    var bar: Bar
10	}
   
11	var foo = Foo(bar: Bar(x: 1, y: 1))
12	foo.bar.x = 10

Maybe not in the way it was written. Here:

struct Struct {
    var litmus = Litmus()
    var int = Int()
}

class C {
    var s = Struct()
}

var c = C()

No matter what compiler does on "c.s.int += 1" mutation, the modified struct is going to be at the same exact location in memory. If, as you say, "compiler creates a new copy of Struct" it will then have to memcpy that copy to the original place. It might theoretically do this as it is unobservable by standard language means (*) other than by observing resulting performance degradation.

(*)

you can set some unmapped memory islands at the beginning and end of S to see if compiler is actually reading the whole "S" at the point of mutation in s.int += 1 where the "int" variable is in the middle of the said struct. That will work only for large bigger than page size structs:

struct Struct {
    var prefix: page size of something here
    var int: Int
    var 1page size of something here - Int padding here
    var trailing: page size here
}
1 Like

One should not be considering the implementation details of the type when using the type. That is, one should not assume that because the type doesn't (appear to) use COW that it will therefore be cheap to mutate. It's better to assume the copy will be made, and only if one is optimizing should one use the various compiler directives and hints to ensure in-place and optimized mutation.

Thinking this way is also more consistent. For example, inout parameters are copied and the copy assigned back to the original. It may be possible for the compiler to optimize away the actual copies, but it's less likely than in other scenarios.

1 Like

Can you please clarify what Copy-on-write covers in the context of the Swift language?

Until reading this thread, I thought that Copy-on-write is a technique used in Swift collections, where the data are stored in an heap-allocated buffer, which is checked upon modification whether it is uniquely referenced[1]. If so, a copy is made. The compiler than uses an optimization step known as " Stack Promotion of Swift Reference Types"[2] to optimize-out superfluous heap allocations.

My assumptions above are further reinforced by the Copy-on-Write section of SIL documentation[3]. The documentation at the same time made me think, that the full Copy-on-Write behavior may be possible only for "standard" collections and my custom types that would use isKnownUniquelyReferenced(_:) to implement their data buffet won't have the same degree of optimization.

After reading this thread, I am a bit confused about what (how many) techniques we think about, when talking about Copy-on-Write in Swift :thinking:

Thanks for this thread!

[1] isKnownUniquelyReferenced(_:) Apple Developer Documentation
[2] Value Types and Reference Types in Swift
[3] Copy-on-Write Representation

1 Like

Any type can implement CoW semantics. Using isUniquelyReferenced(_:) to avoid unnecessary copies is just an optimization in the implementation. Logically, mutating in-place when the reference is unique is equivalent to creating a temporary copy, mutating it and then assigning it back to the original reference.

I can't speak of the compiler's ability to automatically avoid copying for non-stdlib types.

1 Like

To learn more about structs and mutability, I would recommend reading (or re-reading, if it's been a while) the evolution proposal describing the law of exclusivity (SE-0176).

extension Int {
  mutating func assignResultOf(_ function: () -> Int) {
    self = function()  
  }
}

var x = 0

// CONFLICT.  Calling a mutating method on a value type is a write access
// that lasts for the duration of the method.  The read of 'x' in the closure
// is evaluated while the method is executing, which means it overlaps
// the method's formal access to 'x'.  Therefore these accesses conflict.
x.assignResultOf { x + 1 }

Basically, we have a concept of non-instantaneous, mutable accesses to a struct. Accessing a field of the struct is also an access to the entire enclosing struct.

We also have _modify accessors, which are used to implement such non-instantaneous, mutable accesses. We are not limited to get and set operations (kind-of; _modify is not yet a formal language feature, but is used basically everywhere).

We may also one day have "borrow variables" which allow you to extend those access even further.

If the mutating keyword was not enough, all of these other features strongly imply that structs are indeed mutable. The compiler may make copies in some situations when structs are used in particular ways, but rules such as the law of exclusivity are designed explicitly to provide the guarantees that allow for in-place mutation.

2 Likes

Can someone explain me what's wrong here?:

// These are simple global variables.
var global: Int = 0
var total: Int = 0

extension Int {
  // Mutating methods access the variable they were called on
  // for the duration of the method.
  mutating func increaseByGlobal() {
    // Any accesses they do will overlap the access to that variable.

    total += self // Might access 'total' through both 'total' and 'self'
    self += global // Might access 'global' through both 'global' and 'self'
  }
}

The reason I don't quite understand what's wrong here, is that when executing:
self += gobal
Well, shouldn't that not violate the law of exclusivity since even if self is global, the right hand side is read-only?

If you call that function on global itself, you will have overlapping write-read accesses, since:

So self is considered as being modified for the entire body of the function. Within the function, you can read/write any instance properties on self; they are not considered overlapping, because you're already within a non-instantaneous access.

However, if you access the same variable but don't use self (e.g. you access it via global), that will begin a new access. That access conflicts with the access you're already in. It doesn't matter if you read/write from global; as you're already within a modify/write access to the same variable, all other accesses are considered conflicts.

two accesses to the same variable are not allowed to overlap unless both accesses are reads

(Note: the law of exclusivity was later relaxed to allow overlapping accesses if both are atomic. It doesn't matter for this question, I'm just mentioning it for completeness)

1 Like

Note that in this example, Got a new bar! will be printed, even though we didn't ever call foo.bar = something. This is because changing foo.bar.x semantically creates a new Bar value, and thus Foo needs to hear about it. Even though the actual modification of the value in memory likely happens in-place, at a semantic level the entire Bar struct is being replaced with an entirely new value.

3 Likes

Thanks! I think your example and explanation is the most simple and clear one.

Another thing about property observers that makes this even more clear is that in didSet you're able to reference oldValue. In this case, the compiler must make a full copy of the value since the user is able to reference both versions of the value at the same—an in-place modification wouldn't allow this.

2 Likes

Good point. Does the current compiler do a full copy always? Or only in the presence of willSet / didSet? Or only if newValue / oldValue are actually getting used in willSet / didSet?

See this thread:

2 Likes