Go-1-2-Proposal---Immutability
Go-1-2-Proposal---Immutability copied to clipboard
Copying immutable scalar types to their mutable counterparts
I discovered a major problem that requires an exception to the prohibition of immut -> mut
casting. This exception is necessary to make copying of immutable scalar types to their mutable counterparts possible which would be impossible with the first revision of the proposal specification.
Problem Demonstration (Immutability by default)
//go:immutable
func TakeMut(mutableInt mut int) { /*...*/ }
func main() {
immutableInt := 42
TakeMut(immutableInt) // Compile-time error
var mutableInt mut int = immutableInt // Compile-time error
TakeMut(mutableInt)
}
Problem Demonstration (Mutability by default)
func TakeMut(mutableInt int) { /*...*/ }
func main() {
var immutableInt immut int = 42
TakeMut(immutableInt) // Compile-time error
var mutableInt int = immutableInt // Compile-time error
TakeMut(mutableInt)
}
We can't pass immut int
as mut int
because casting immut
to mut
is illegal. Creating a temporary mutable variable is impossible for the exact same reason.
Solution
immut -> mut
two-way casting of scalar types is considered safe because scalar types are copied and cannot lead to mutable shared state. This exception rather extends the previously specified one for the implicit two-way casting of pointer-receivers previously described in section 2.12.1.
For pointer types this exception would only apply to the pointer itself (because pointers are regular scalar types), but not the types of the objects it points to:
// Immut_Immut takes a reference to an immutable integer
// while the pointer itself is also immutable in the context of this function
func Immut_Immut(i *int) {
// i can be passed both "immut * immut int"
// and "mut * immut int"
}
// Mut_Immut takes a reference to an immutable integer
// while the pointer itself is mutable in the context of this function
func Mut_Immut(i mut * immut int) {
// i can be passed both "immut * immut int"
// and "mut * immut int"
}
// Mut_Mut takes a reference to a mutable integer
// while the pointer itself is mutable as well (in the context of this function)
func Mut_Mut(i mut *int) {
// i can be passed both "mut * immut int"
// and "immut * immut int"
}
// Immut_Mut takes a reference to a mutable integer
// while the pointer itself is immutable in the context of this function
func Immut_Mut(i * mut int) {
// i can be passed both "mut * immut int"
// and "immut * immut int"
}
func main() {
immutInt := 42
mutInt := mut(42)
mutPtr_immutInt := mut(&immutInt)
mutPtr_mutInt := mut(&mutInt)
/*
Mutable and immutable pointers are interchangable because they're scalar and thus copied.
This is fine as long as the pointed to type is casted from mut to immut
or doesn't require any casting at all
*/
Immut_Immut(immutPtr_immutInt) // Implicit cast
Immut_Immut(mutPtr_immutInt) // Implicit cast
Mut_Immut(mutPtr_immutInt) // Implicit cast
Mut_Immut(immutPtr_immutInt) // Implicit cast
Immut_Immut(immutPtr_mutInt) // Implicit cast
Immut_Immut(mutPtr_mutInt) // Implicit cast
/*
We cannot however pass a pointer to an immutable type
if the pointed to type will need to be casted from immut to mut!
*/
Mut_Mut(immutPtr_immutInt) // Compile-time error
Mut_Mut(mutPtr_immutInt) // Compile-time error
Immut_Mut(immutPtr_immutInt) // Compile-time error
Immut_Mut(mutPtr_immutInt) // Compile-time error
}
This points to a deeper problem. Immutable scalar types do not make much sense, since scalar types are always copied. Only non scalars, i.e. arrays, slices, structs passed by pointer or containing pointers and maps can actually be mutated. It follows then that immutability on scalars is not neccesary.
@beoran There is no problem.
Why immutable scalar types are inevitable - Part 1
Scalar types are not immutable because they can still be reassigned. Immutable scalar types cannot be assigned another value, therefore they're inevitable. For example, if you want to create functional-style immutable objects you can make use of immutable scalar field types:
// ImmutableObject represents an immutable object
// the fields of which cannot be changed after initialization
type ImmutableObject struct {
ID immut string
Number immut int
}
// NewImmutableObject creates a new immutable object
func NewImmutableObject() ImmutableObject {
return ImmutableObject{
ID: NewUUIDv4(),
Number: 42,
}
}
func main() {
obj := NewImmutableObject()
obj.ID = "new identifier" // Compile-time error
obj.Number = 24 // Compile-time error
}
Any type can be immutable and as you can see it totally makes sense. You might ask: "But we can make the returned object immutable?!"
type Object struct {
ID mut string
Number mut int
}
// NewImmutableObject creates a new immutable object
func NewImmutableObject() immut Object {
return ImmutableObject{
ID: NewUUIDv4(),
Number: 42,
}
}
func main() {
obj := NewImmutableObject()
// The fields will still be contextually immutable
obj.ID = "new identifier" // Compile-time error
obj.Number = 24 // Compile-time error
}
While this approach would deliver almost the same results - the problem becomes apparent when you try to mix immutable and mutable types in a single structure to prevent the modification of only certain fields:
// MixedMutabilityObject represents a partially mutable object.
// You may reassign number, but you can't reassign ID
type MixedMutabilityObject struct {
ID immut string
Number mut int
}
// NewMixedMutabilityObject creates a new immutable object
func NewMixedMutabilityObject() MixedMutabilityObject {
return ImmutableObject{
ID: NewUUIDv4(),
Number: 42,
}
}
func main() {
obj := NewMixedMutabilityObject()
obj.ID = "new identifier" // Compile-time error
obj.Number = 24 // Fine
}
This is one of the cases when immutable scalar types are useful, but there are also others like immutable variables etc.:
func Func(a immut int) {
var b immut int = a * 2
a = 50 // Compile-time error
b = 60 // Compile-time error
}
Why immutable scalar types are inevitable - Part 2
Scalar types contained in slices or maps are still addressable and can therefore be mutated:
var s immut [] mut int = []int {1, 2, 3}
// Operations on the immutable slice
s = append(s, 10) // Compile-time error
s[0] = 10 // Compile-time error
// Operations on the mutable scalar items
s[0]++ // Fine
f := &s[1]
*f = 4 // Fine
log.Print(s) // [2, 4, 3]
To prevent this kind of mutations we need to declare the scalar items immutable as well (make them inherit the immutability of the slice).
var s immut []int = []int {1, 2, 3}
// Operations on the immutable slice
s = append(s, 10) // Compile-time error
s[0] = 10 // Compile-time error
// Operations on the mutable scalar items
s[0]++ // Compile-time error
f := &s[1] // * immut int
*f = 4 // Compile-time error
log.Print(s) // [1, 2, 3]
Copying of immutable scalars to their mutable counterparts
The only problem this issue describes is the inability of copying immutable scalars to their mutable counterparts because of the initial prohibition of immut to mut casting. I already described a similar case for pointer-receivers in section 2.12.1 but I somehow totally forgot about other scalar types (pointers are just regular scalar types), so I just need to expand the exception to the prohibition of immut -> mut
casting from receiver-pointers only to scalar types in general.
Note what I am talking about are values, not types or variables. That is actually something important to consider, the difference between those three. Integers, floats, pointers and go strings are immutable values already. What go does not have are tuples (immutable array values), named tuples (immutable strict values), and immutable map values.
I condsider immutability of variables far less usefuly than the immutability of their values.
@beoran
Think of types as interfaces to memory.
A mut int
is an interface to 4/8 bytes in memory with methods like Read
, Set
, Increment
and Decrement
. An immut int
is an interface with the Read
method only, you can't reassign or mutate the 4/8 bytes behind an immut int
because the interface is read-only.
Again, don't think about "immutable variables", there's no such thing in this proposal, rather think about "immutable types" as "write-protected interfaces to potentially shared memory". Memory can be shared not only by goroutines, it can also be shared by individual scopes.
Scalar types actually never share their memory (except strings). All scalar types, except strings, own their memory, but they're still mutating interfaces to their own memory, which is not always ideal. Constants can't always replace immutable scalar types because they're determined at compile-time, while immutable scalar types can be determined at runtime.
What go does not have are tuples (immutable array values), named tuples (immutable strict values), and immutable map values.
Because Go doesn't need those. You can easily implemented any kind of immutability you want using the immutable type paradigm.