-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: Go 2: support read-only and immutable values in Go #31464
Comments
Reading the proposal, I kind of like the idea of having |
How to do it without any hints for compilers? I think there must be some hints for compilers to prevent some behaviors. BTW, the syntax is |
I do think the |
I think so, after all a final value is read only by definition. Please consider updating your proposal with this in mind. |
It is not that simple. Final values are also reader values has its only drawbacks and problems. The complexity in the current one has its rationals. I will try to make a new one for final values are also reader values when I think it clearly. |
Hi, author of proposal: Go 2: immutable type qualifier here! First, I'm glad to hear people talk about this topic more and more! Go lacks important compiler-enforced safety features and while I can live without compile-time memory safety (O&B) - living without read-only types is dangerous. My proposal is currently on hold since I don't have much time investigating the const-poisoning problem which is a blocker and I hope you heard of it. 1. a
JavaScript's I've gone the other way in my proposal: an immutable type makes all its members immutable, which make all their members immutable, recursively. If you see 2. The more exceptions there are to a rule - the worse the rule is.
IMO, in an imperative (non-functional) language mutability is a property of types, not a property of values. A type may reference a value but it can't protect it from other mutable types referencing it. Go is not a functional language, it can't guarantee value immutability (FP-style immutability can have significant performance impacts and would be unacceptable in case of Go), it can only protect memory from being mutated through certain references ultimatelly making the code easier to read and debug. The only immutable values in Go are constants, but those are primitive-types only for good reasons. If immutability is a property of types, then why can't we make this rule universally applicable to all types without exceptions? 3. What happens, if the developers needs this kind of fine-grained control over what's mutable and what's not in a type-chain? Exactly, he/she will throw immutability out the window if it's too limiting and this is obviously not what we'd want to happen I suppose. A safety-feature that covers only simple cases but fails to cover rather complex cases isn't a safety feature. 4. The
BTW, I called this feature Mutability Qualification Propagation, it reduces verbosity quite substantially. P.S. |
var s = []int{1, 2, 3}
final x = []int{1, ,2 ,3}:reader // the elements referenced by x are immutable
final y = s:reader // the elements referenced by y are read-only (through y).
final z = s // the elements referenced by z are writable (through z) Please note the I agree JavaScript's
I agree mutability can be viewed as a property of types, but I think read-only is mainly a property of values. As I have said in my proposal,
The reason is the following notations are all invalid: []([]int:reader):reader
*(T:reader):reader
struct {
x T:reader
}
// including the ones you made:
[]:reader *:reader T:reader
[]:reader * T:reader
[]:reader *:writer T:reader A
This is what my proposal tries to avoid, for this brings much complexity and such needs are rare in practice. And I'm some confused by your description. In your first example,
Sorry, the following notations are invalid by my proposal. // proposal 31464:
var x []:reader *:reader T:reader
var y []:reader * T:reader they should be: // proposal 31464:
var x []*T:reader
var y []*T:reader And there is not a var y []:reader *:writer T:reader |
There won't be any "real" immutability in Go because it's not a functional language, actual immutability can only be achieved in languages that don't support the assignment operator. Introducing immutability would imply introducing PDS (Persistent Data Structures) to improve performance, otherwise we might end up having to copy the hell out of every
I'm strictly against such kind of magic, because it's misleading and error prone! It makes a variable look immutable, but it doesn't make it immutable because a variable value isn't just the address of the referenced data, it's the data itself (recursively).
Actual immutability is a nice concept (especially for application programming), it makes code readable and safe, but since Go is a general purpose non-FP language and does support assignments, hence it's better to leave real immutability out of Go to keep it simple and consistent and stick to read-only types IMHO. This way you could, for example, write your own PDS libraries using read-only types (in combination with generics this could be great) but the language itself doesn't force you to follow any particular concept. I refer to "immutable" when a value is referenced by read-only references exclusively and is thus virtually immutable (you can only modify it through unsafe pointers, but this is okay, we can't forbid people to shoot themselves in the foot, it's called "unsafe" for a reason). If you want to make a variable read-only then make it have a read-only type and use var s = []int{1, 2, 3}
var x = immut []int {1, 2 ,3} // the elements referenced by x are immutable
y := immut []int(s) // the elements referenced by y are read-only (through y).
h := [] immut int(s) // the elements are writable, but the slice is not. IMO this is better, because it's much more readable, you can see what's mutable and what's not, you don't have to assume: w := &T{} // mutable
r := immut *T(w) // write-protected (recursively)
v := immut * mut T(w) // address is read-only, but the underlying data is writable
Again, Go is a non-FP language, there won't be immutable values, because it's fundamentally imperative and supports assignment. You could mutate a seemingly immutable value through unsafe pointer and all hell will break loose. We can't make Go an FP language and we shouldn't try to make it pseudo-FP. There won't be immutable values in Go.
That's where our opinions differ. I'd prefer a flexible tool, that could safe me in complex situations (but I'd still try to avoid complex situations as much as possible of course), rather than a toy, that's okay for 90% of cases but practically useless in the really difficult 10%, this ain't a safety feature then IMO.
This is called MQP (Mutability Qualification Propagation). MQP reduces verbosity, but gets rid of transitive qualifiers. It may not be obvious at the first glance but it's very simple: a mutability qualifier propagates to the right in a type chain, until it's canceled out by another qualifier: // with MQP
var p immut *T
var m immut map[string][]string
var x immut [][] mut T
// without MQP
var p immut * immut T
var m immut map[immut string] immut [] immut string
var x immut [] immut [] T
I got that, that's why I explicitly stated: "Even if you make immutability a property of types it still doesn't particularly look very sexy IMHO:". But I should probably have written: "Even if you remove transitive qualification"
I know, it was just a hypothetical example. |
Yes, we can use Imperative immutability is a real need of many Go programmers. It is very useful. I think you have got it clearly what are the differences between the two proposals. The main intention of my one is to keep both the syntax and concepts simple, which sticks to Go style. Let's agree on what we agree and disagree on what we disagree. :) |
Sure, I just wanted to emphasize that in an imperative language where assignment is allowed - immutable values don't exist by nature. There may be read-only references and virtually immutable objects (referenced by read-only references exclusively), but no actually immutable values/memory.
I do absolutely agree. Makes code safer and APIs clearer. It does have some drawbacks like const-poisoning which I couldn't yet investigate but it's probably solvable.
Those aren't all differences I suppose, but I didn't have the time to investigate it in full detail yet. Please correct me if there's something wrong/missing.
Those are good intentions, but I disagree on the way you're trying to achieve them. IMO immutable/read-only types are easier and way more consistent than having two concepts There's also some inconsistencies like:
Why? we already have read-only channels in Go. Why do we need yet another variant? |
Something to note:
It is not a special rule. It is just a consistent general rule for all kinds of types in this proposal, it is not channel specified. It is just that final values are not modifiable, whatever their types are. There are not no-directions channels, right? Final is a stronger property than single-direction. |
What do you mean by that? MQP is necessary for mixed mutability type chains to reduce verbosity. If I understood correctly there's no mixed qualification with
What I'm talking about is this: type User struct {
Name immut string
BirthDate immut *time.Time
Devices immut [] mut Device
}
u := U{
Name: "initial name",
BirthDate: nil,
Devices: []Device{Device{Name: "initial name"}}
}
u.Name = "new name" // compile-time error!
u.Devices[0] = Device{Name: "new device"} // compile-time error!
u.Devices[0].Name = "new device name" // OK
If |
OK, I misused it. It should be IMQP, immutability Qualification Propagation. :)
I deliberately discarded the partial read-only/immutability feature. Please read the end of my proposal for details. This feature brings many confusions and complexities.
I don't like the |
We should inspect channels in logic, not in the internal implementation. Channels are different to slices and maps. For example, a |
Thanks for the detailed proposal. The
It's not obvious to me how The number of extra rules necessary to make this work, even assuming that it is complete, seems very large compared to the benefit to the rest of the language. The problem area remains interesting to investigate, but we aren't going to adopt this proposal. |
@ianlancetaylor There is not the
I don't very understand this question. Could you show an example to help me understand it?
I don't think the number of extra rules in this proposal is large. The rule set is quite small IMHO.
In fact, I have an alternative proposal which makes a little modification on this one. |
I will reopen this issue because you requested it. But I want to be clear that I see no possibility of anything like this proposal being adopted for Go. I asked questions not because the answers affect the decision for this proposal, but as pointers for future work in a different direction. The |
@beoran @romshark // By the current rule, we can't send values to (or receive values from) c.
final c = make(chan int, 1)
// p is deduced as a reader value
var p = &c
// c1 is deduced as a reader value.
// By the current rule, we can send values to (or receive values from) c1.
// It is broken!
var c1 = *p In the next version (v9.a), the final value concept will be removed, also the related channel rules. As some flaws are found in this current version (v9.1), and the difference between v9.1 and the next version v9.a is large. I will close this proposal and make a new one. |
The new proposal is here: #32245 |
Abut read-only-poisoning, I think it is really a problem, but it is not a serious problem. Read-only memory zone has negative impacts on run-time performance. |
The full proposal is here: https://github.com/go101/immutable-value-proposal/blob/master/README-v9.1.md
Basically, this proposal can be viewed as a combination
of issue#6386
and issue#22876.
This proposal also has some similar ideas with
evaluation of read-only slices written by Russ.
The text was updated successfully, but these errors were encountered: