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
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.


Immutable struct fields are declared using the `const` qualifier. Immutable
fields can only be set during the definition of the object and are then
immutable for the entire lifetime of the object within any context.
Expand Down Expand Up @@ -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.


Immutable methods are declared using the `const` qualifier on the function
receiver and guarantee to not mutate the receiver in any way when called.
They can safely be used in immutable contexts, such as within other
Expand All @@ -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.

o.mutableField = &Object{} // Compile-time error
return o.mutableField
}

Expand Down Expand Up @@ -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.


Immutable return values are declared using the `const` qualifier and guarantee
that the returned objects will be immutable in any receiving context.

Expand Down Expand Up @@ -362,4 +371,4 @@ considered of higher priority compared to other previously mentioned topics.

----
Copyright © 2018 [Roman Sharkov](https://github.com/romshark)
(<[email protected]>)
(<[email protected]>)