Tuesday 24 February 2015

When a Swift immutable collection isn't or is at least immutable-ish

Take the following code:

 struct Foo  
 {  
     var value: Int  
 }  
   
 let foo = [Foo(value: 1), Foo(value: 2)]  

By using 'let foo' an immutable array has been created. As such no members of this array can be replaced and nor can an element's contents be modified. As such both statements below result in compilation errors.

 foo[0] = Foo(value: 777) // error!  
 foo[0].value = 8 // error!  

If foo were declared var then both of the above statements would work.

This changes slightly when using reference types:

 class Bar  
 {  
     var value: Int = 0  
       
     init(value: Int)  
     {  
         self.value = value  
     }  
 }  
   
 let bar = [Bar(value: 1), Bar(value: 2)]  
   
 bar[0] = Foo(value: 777) // error!  
 bar[0].value = 8 // allowed  

The first case of trying to replace an element fails as before but modifying the instance referenced by that element is permitted. This is because the immutable collection holds a reference to the instance of Bar. Modifying the instance does not change the reference so the collection remains unchanged.

If you're making an effort to make your code as immutable (const) as possible (I'm from a C++ background so I endeavour to making everything as const I can) then this is a gotcha to lookout for.

The only way to make Bar properly const is to provide a private setter for the member, assuming it's defined in it's own source file (so this example doesn't work in a Plaground), i.e.

 class Baz  
 {  
     private(set) var value: Int = 0  
       
     init(value: Int)  
     {  
         self.value = value  
     }  
       
     func f()  
     {  
         value = 88;  
     }  
 }  

Which now prevents value being assigned too.

 let baz = [Baz(value: 1), Baz(value: 2)]  
 baz[0].value = 8 // error!  
   

Even if all the properties have private setters there might be methods on the reference type that modify the instance, such as f() above. Just having an immutable collection of reference types is not sufficient to stop these mutating the instances referred too. Invoking f() in the above collection will change the value to 88, i.e.

 println("\(baz[0].value)") // Gives 1  
 baz[0].f()  
 println("\(baz[0].value)") // Gives 88  
   

This differs to C++ where if a collection is const or is returned as a const reference (from a method) than not only is the container immutable but so are its contents. It would be good if Swift were to gain a mechanism that would mark the collection and its contents completely immutable other than having to use a value type.

Anyway, beware of making a collection of reference types immutable and assuming that both the collection and its contents cannot be modified!

No comments: