Skip to content
This repository has been archived by the owner on Aug 28, 2023. It is now read-only.

Go 2: mutability qualification propagation #20

Open
romshark opened this issue Oct 5, 2018 · 8 comments
Open

Go 2: mutability qualification propagation #20

romshark opened this issue Oct 5, 2018 · 8 comments
Assignees
Labels
discussion This issue is being discussed help wanted Extra attention is needed problem A general logical problem
Milestone

Comments

@romshark
Copy link
Owner

romshark commented Oct 5, 2018

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:

  • Mutability qualification propagates down the type chain to the right, until either another qualifier cancels it out, or the propagation path ends.
  • Map-key type definitions branch off the root propagation path creating another one.

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:

  • 3 qualifier propagation paths: the root, the first key-map branch and the second key-map branch.
  • 5 mutability qualifiers.
  • 3 qualifier cancelations.
  • 6 mutable and 5 immutable compound types.

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
@romshark romshark added problem A general logical problem discussion This issue is being discussed labels Oct 5, 2018
@romshark romshark added this to the v2 milestone Oct 5, 2018
@romshark romshark self-assigned this Oct 5, 2018
@romshark romshark changed the title Go 2.x: mutability qualification propagation Go 2: mutability qualification propagation Oct 5, 2018
@romshark romshark added the help wanted Extra attention is needed label Oct 5, 2018
@deanveloper
Copy link

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
Copy link
Owner Author

romshark commented Oct 5, 2018

@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
Copy link

beoran commented Oct 5, 2018

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
Copy link

therealplato commented Oct 5, 2018

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
Copy link

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
Copy link

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

@romshark
Copy link
Owner Author

romshark commented Oct 5, 2018

@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
Copy link

beoran commented Oct 5, 2018

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
discussion This issue is being discussed help wanted Extra attention is needed problem A general logical problem
Projects
None yet
Development

No branches or pull requests

5 participants