-
Notifications
You must be signed in to change notification settings - Fork 4
Update README.md and some questions/comments #3
base: master
Are you sure you want to change the base?
Conversation
@@ -76,6 +76,9 @@ versions of the Go 1.x programming language will continue to compile and work as | |||
expected. | |||
|
|||
### 2.1. Immutable Fields | |||
|
|||
*onokonem: do we really need the immutable fields in mutable struct? what for?* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Immutable fields are useful in those cases when we want to ensure, that certain fields don't change during the entire lifetime of an object once they've been set by the constructor during the initialization.
Imagine that we'd want to implement a Factory
type, that has a Create() UniqueObject
method that creates unique object instances. Each object would, therefore, be supplied with a unique identifier: type UniqueObject struct { Ident string }
.
In Go 1.11 we must make the identifier private and write a getter function Identifier() string
to prevent the identifier from being changed during its lifetime.
Though what we essentially want to achieve here is actually an immutable field.
Not only do we want to ensure, that UniqueObject.Identifier
isn't mutated from the outside - we also want to prevent it from being mutated inside private methods and the package scope, that's why we'd want to have immutable struct fields.
type UniqueObject {
internal const string
}
// NewUniqueObject creates a new unique object
func NewUniqueObject() UniqueObject {
return UniqueObject{
// Immutable once initialized
internal: newUUIDv4(),
}
}
// SomePackageMethod can't violate the immutability constraints
// even having access to the internals of the struct
func SomePackageMethod(uo *UniqueObject) {
io.internal = "garbage" // Compile-time error
}
Also consider this: Why are we writing "dumb" getter methods in Go 1.x? ...Exactly! Because we can't just declare an exported but immutable field, so we have to emulate it using an unexported mutable field, and a method, that makes the verbal, insidious promise to not change it just returning a copy to the outside! It's insidious because the compiler doesn't guarantee that we (our colleagues, or the guys pushing their pull request on your github repo) can't mutate your internal field in the scope of our package! If we do - we wouldn't even know, and that's dangerous!
Conclusion: when you declare a new struct type, you always know intuitively what should be exported and what shouldn't. Same principles apply to mutability, you almost always know what's never going to change during the entire life-time of your object, so why not ensure, with a const
constraint, that it's not changed by anyone anywhere anyway?
P.S. I should add this to the main proposal document to clarify, why we really need immutable fields.
@@ -114,6 +117,9 @@ func main() { | |||
|
|||
---- | |||
### 2.2. Immutable Methods | |||
|
|||
*onokonem: should we rename this one to immutable receivers?* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not quite sure yet.
By saying immutable method
I try to clarify, that a function with a const receiver can be used in an immutable context, such as inside another immutable method, and/or on immutable variables, arguments, fields etc. In C++, for example, they're called "const-methods".
I also stated, that, yes, technically this should be called "immutable receivers", though when explaining why we can execute a function with an immutable receiver on an immutable argument, speaking of "immutable methods" might be more intuitive and thus easier to understand.
@@ -137,8 +143,8 @@ func (o *Object) MutatingMethod() const *Object { | |||
// It's illegal to mutate any fields of the receiver. | |||
// It's illegal to call mutating methods of the receiver | |||
func (o const *Object) ImmutableMethod() const *Object { | |||
o.MutatingMethod() // Compile-time method | |||
o.mutableField = &Object{} // Compile-time method | |||
o.MutatingMethod() // Compile-time error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops, that's right.
README.md
Outdated
@@ -197,6 +203,9 @@ func ReadObj( | |||
|
|||
---- | |||
### 2.4. Immutable Return Values | |||
|
|||
*onokonem: do we really need the immutable fields in mutable struct? what for?* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait, this is about immutable fields, not about immutable return values, right? I suppose it's a mistake.
Though let me clarify why we'd need immutable return values anyway.
type Engine struct {}
// Shutdown is a non-const method
func (e *Engine) Shutdown() {
// Initiate engine shutdown
// BLock until engine is shut down
}
// ReadTemperature is a const-method
func (e const *Engine) ReadTemperature() (int, error) {
// Read the temperature, do tricky stuff
return 42, nil
}
type Car struct {
engine *Engine
}
// GetEngine is a const-method, it won't mutate the engine
func (c const *Car) GetEngine() const *Engine {
// Return immutable reference to the engine
return c.engine
}
func main() {
car := Car{
engine: &Engine{},
}
engine := car.GetEngine()
// GetEngine returns a read-only reference
// we can't stop the engine this way, because we're not allowed to for a reason!
engine.Shutdown() // Compile-time error
// Reading the temperature though is just fine, because it's a const method
temperature, _ := engine.ReadTemperature()
}
The above code represents a case, where we want to return an immutable reference to a mutable internal field. Without immutable return values this wouldn't be possible. It's not the best example, but the concept should be clear.
@@ -197,6 +203,9 @@ func ReadObj( | |||
|
|||
---- | |||
### 2.4. Immutable Return Values | |||
|
|||
*onokonem: do we really need the immutable return values? what for?* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm also interested in seeing explanation why immutable return value is needed.
(Especially in combination with const fields.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sometimes you want to prevent something that you return to be mutated by the caller.
If, for example, you return an internal slice of objects from a struct method and you want to ensure the caller can't mutate neither the slice nor the objects inside to preserve encapsulation:
type Client interface {
const RemoteAddress() string
Close() error
}
type Server struct {
connectedClients []Client
}
// GetConnectedClients is an immutable method that returns a
// deeply immutable reference to the internal slice of clients
func (s * const Server) GetConnectedClients() const [] const Client {
return const [] const Client(s.connectedClients)
}
func main() {
server := Server{}
clients := server.GetConnectedClients()
// We can read the clients
log.Print("connected clients: ", len(clients))
log.Print("first clients IP:", clients[0].RemoteAddress())
// But we can't mutate them and call mutating methods
clients[0] = nil // Compile-time error
clients[0].Close() // Compile-time error
}
Currently we'd have to:
- copy the entire slice before returning it to the outside of the struct scope to avoid nasty aliasing, which is both clunky and inefficient
- and define an immutable Client interface with
func (s *Server) GetConnectedClients() []ReadOnlyClient {
connectedClients := make([]ReadOnlyClient, len(s.connectedClients))
for i, clt := range s.connectedClients {
connectedClients[i] = clt.(*client)
}
return connectedClients
}
Finally, immutability is all about immutable types.. wherever you can use types - you can use immutable types, be it a return value or something else. This is both consistent and very helpful as it makes avoiding mutable shared state possible.
No description provided.