romshark / Go-1-2-Proposal---Immutability

A a Go 1/2 language feature proposal to immutability
https://github.com/golang/go/issues/27975
171 stars 5 forks source link

Go 2: mutability qualification propagation #20

Open romshark opened 5 years ago

romshark commented 5 years ago

The Qualification Verbosity Problem

A lot of people reject both mut [] mut [] mut * mut T and const [] const [] const * const T which is totally understandable, I don’t like this level of verbosity either. In fact, I hate it myself, but until now I didn’t really have another option to offer and considered it a "dirty, but necessary bureaucratic evil". I thought about it and I might have come up with a solution, which, unfortunately is only possible in Go 2.x (the reason is explained below) and is also yet to be discussed.

Problem: Verbosity vs Mixed-Mutability Types

Most would prefer transitive immutability, where the following type definition is deeply immutable:

const [][]*T

With transitive immutability, the above type definition would represent:

an immutable slice of | immutable slices of | immutable pointers to | immutable Ts

Compared to const [] const [] const * const T it's way less verbose and much more readable. But it didn’t answer the question:

“But what if I need the T in the end to be mutable? Do I have to throw immutability out the window then and make everything mutable again?“.

Transitive immutability reduces verbosity, but introduces another problem: it makes mixed-mutability types impossible to describe voiding the entire concept of immutable types! Developers will avoid immutable types in situations involving complex mixed-mutability types, but those are the situations they need the help of the compiler most! An immutability concept that works only in simple cases but fails to be useful in rather complex situations is simply pointless.

Potential Solution: Mutability Qualification Propagation

The concept of "mutability qualification propagation" (short: "MQP") attempts to reduce verbosity while still allowing mixed-mutability types. It's based on two fundamental rules:

MQP: A Simple Example

Consider the following deeply immutable two-dimensional slice of pointers to T:

[][]*T

The above type definition implies immutability by default and represents:

an immutable slice of | immutable slices of | immutable pointers to | immutable Ts

To make the entire type deeply mutable we need to prepend the mut qualifier only ones:

mut [][]*T

The qualifier will propagate to the right and affect every type in the type chain! The above type definition now represents:

a mutable slice of | mutable slices of | mutable pointers to | mutable Ts

To make an exception for the T at the end and make it mutable, we need to cancel out the first mut qualifier by an immut qualifier the following way:

mut [][]* immut T

The above type definition now represents:

a mutable slice of | mutable slices of | mutable pointers to | immutable Ts

MQP: A Ridiculously Complex Example

The following ridiculously complex example demonstrates, that this system consistently works across all levels of type complexity.

DISCLAIMER: THIS CODE IS FOR DEMONSTRATIONAL PURPOSES ONLY, DO NOT EVER WRITE CODE LIKE THIS IF YOU DON'T WANT TO END UP ON THE GUILLOTINE!

Consider the following type definition:

mut map [ [3] immut * mut T ] [] * [] immut map [T] immut * T

The above type definition implies immutability by default and represents:

a mutable map of (mutable arrays of immutable pointers to mutable Ts)-> to mutable slices of mutable pointers to mutable slices of immutable maps of (immutable Ts)-> to immutable pointers to immutable Ts

This type definition consist of:

mqp3

  1. The first mut propagates until the second immut of the root path as well as until the first immut in the first map-key branch.
  2. The first immut in the first map-key branch propagates until it’s canceled out by the second mut.
  3. The second mut propagates until the end of the first map-key propagation branch path.
  4. The second immut actually propagates all the way down to the end of the root propagation path. It’s canceled out by the third immut, but the third one has practically no effect because it's equal to the one it cancels out (The linter should complain about the third immut point out that's it's pointless)

MQP & Go 1.x

Mutability qualification propagation cannot be implemented in the Go 1.x immutability proposal because it requires another keyword (namely: mut) that'd be able to cancel out the propagating const. An additional keyword would require backward-incompatible changes and can't therefore be considered.

MQP & Go 2.x

Mutability qualification propagation can be implemented in the backward-incompatible Go 2.x language specification by adding two new keywords: mut and immut. Both qualifiers are able to cancel each other out.

Qualifier Keyword Naming

immut was chosen because imut is visually very similar to mut and can lead to misinterpretation. Alternatively, nomut could be proposed instead of immut. More naming ideas are always welcome.

MQP vs no MQP

Go 1 Go 2 (no MQP) Go 2 (MQP) Transitive
[][]*T mut [] mut [] mut * mut T mut [][]*T [][]*T
const [] const [] const * const T [][]*T [][]*T const [][]*T
[][]* const T mut [] mut [] mut * T mut [][]* immut T impossible
[][] const * const T mut [] mut [] * T mut [][] immut * T impossible
const map[const * const T] const * const T map[*T]*T map[*T]*T const map[*T]*T
map[const * const T] const * T mut map[*T]* mut T mut map[immut *T] immut * mut T impossible
deanveloper commented 5 years ago

Okay I'm just throwing this out there because it doesn't have too much to do with the proposal itself - but slices aren't comparable and therefore cannot be used as map keys 😅

Anyway, I like that the type system in Go is simple. I don't want to have another language where every case is covered by built-in language features.

romshark commented 5 years ago

@deanveloper that's right, it was meant to be an array actually. Fixed it, need to go get some sleep ASAP.

Let's discuss the question whether or not immutability is necessary in another thread. This issue is all about MQP

beoran commented 5 years ago

This doesn't do much to simplify the type system because now you need three key words in stead of two.
I would say, please think about it in an engineering way. What you seem to want to achieve is to prevent accidental modifications to function arguments. What would be the simplest solution to achieve this?

therealplato commented 5 years ago

~I would quit my job if I had to parse, understand and maintain mut map [ [3] immut * mut T ] [] * [] immut map [T] immut * T. I use go because it feels close to the pareto optimal between (syntax patterns I must understand and use) and (language's capability envelope)~ sorry, link me to the other thread and i'll xpost and delete this

ianlancetaylor commented 5 years ago

The issue tracker is not a great place for general discussion, because discussion is not threaded. the golang-nuts mailing list is a better place.

ianlancetaylor commented 5 years ago

Oh, sorry, this is not on the Go project issue tracker. My apologies.

romshark commented 5 years ago

@beoran What's the third keyword you're referring to? I've only proposed mut and immut here.

Immutable types are not just about "accidental modification to function arguments", they're about accidental modification of anything, be it an argument, a field, a field of a field, a variable, a return value, a function receiver or a package-scope variable. It's about preventing mutable shared state making bugs less likely by making the intentions of the programmer clear.

beoran commented 5 years ago

Well I would say that perhaps we don't need all this detail. Simply have const apply on the whole type and all it's indirections.

I don't think immutsble by default is a good idea. Variables exist to change though time, this is the normal use case. I think languages like Rust get that wrong, the common use case should be the easiest to use.