Go-1-2-Proposal---Immutability icon indicating copy to clipboard operation
Go-1-2-Proposal---Immutability copied to clipboard

Copying immutable scalar types to their mutable counterparts

Open romshark opened this issue 6 years ago • 4 comments

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
}

romshark avatar Oct 09 '18 16:10 romshark

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 avatar Oct 10 '18 03:10 beoran

@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.

romshark avatar Oct 10 '18 14:10 romshark

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 avatar Oct 10 '18 15:10 beoran

@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.

romshark avatar Oct 10 '18 15:10 romshark