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

Update README.md and some questions/comments #3

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

onokonem
Copy link

@onokonem onokonem commented Sep 3, 2018

No description provided.

@romshark romshark added the question Further information is requested label Sep 4, 2018
@romshark romshark self-assigned this Sep 4, 2018
@@ -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?*
Copy link
Owner

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?*
Copy link
Owner

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

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?*
Copy link
Owner

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.

@romshark romshark mentioned this pull request Sep 4, 2018
@@ -197,6 +203,9 @@ func ReadObj(

----
### 2.4. Immutable Return Values

*onokonem: do we really need the immutable return values? what for?*

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

Copy link
Owner

@romshark romshark Sep 23, 2018

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
question Further information is requested
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants