crystal-lang / crystal

The Crystal Programming Language
https://crystal-lang.org
Apache License 2.0
19.29k stars 1.62k forks source link

Immutable instance variables #4359

Open chocolateboy opened 7 years ago

chocolateboy commented 7 years ago

:bulb: Immutable objects are useful because they are inherently thread-safe. Other benefits are that they are simpler to understand and reason about. -- Immutable object

If you want to narrow the type of an instance variable in Crystal you have to assign it to a local variable first, as documented here. This is a kludge which continues to trip users up:

If instance variables could be declared immutable, we wouldn't have to launder them through local variables because they would be guaranteed not to change.

Since we can't introduce variable declarators (presumably), how about a new operator for immutable initialization:

@foo := "bar"

In addition, auto-initialization could be immutable by default e.g.:

Before:

def initialize(@foo : Int32, @bar = 42)
end

def increment_foo
  @foo += 1 # OK
end

def increment_bar
  @bar += 1 # OK
end

After:

def initialize(foo : Int32, @bar = 42)
  @foo = foo
end

def increment_foo
  @foo += 1 # OK
end

def increment_bar
  @bar += 1 # Error
end

See Also

ShalokShalom commented 6 years ago

+1

konovod commented 6 years ago

I don't know about "being default" part (I'll like it but it's a breaking change and can cause confusion. How to autoinitialize mutable fields then?) but there is a lot of cases where some field is assigned only at creation.

sevk commented 6 years ago

keep simple , thanks .

RX14 commented 6 years ago

How did I miss this issue!

It would be possible, I think, for the compiler to detect if an instance variable is immutable or not without changing anything. I'm not sure if it would be a good idea though. Having which ivars are immutable (I'd prefer to call this readonly, as for classes / pointers the contents of the class can change) documented would be great.

ShalokShalom commented 6 years ago

@konovod I think 'recommended' is a nice way

ozra commented 6 years ago

Yeah, set-once ivars is a common pattern, and formally saying so gives both clarification to human reader and low-level optimization benefits to backend. +1.

drosehn commented 6 years ago

Are we talking about immutable variables, or variables which have immutable types once set? The page at docs/if_var is talking about how the type might change within an if statement.

I ask because it seems to me that both kinds of "immutable" could be valuable.

drosehn commented 6 years ago

Hmm. "immutable type" is not a good description of what I'm thinking of. I have instance variables which I initialize as @somevar = nil, but after the initialization I would never re-set the value to nil. So once the variable is given a non-nil value, it will never become nil again. In fact, it'd be a bug in my own logic if anything did set it to nil.

Not sure if the compiler should have explicit support for that, but the idea matches the issue discussed in docs/if-var.

ozra commented 6 years ago

@drosehn - this is immutability regarding value, or well, not even that; just variable/symbol (the value can still be mutated [depending on our definition of value in this context], but you can't reassign the variable), that's what I was referring to at least.

Regarding the type inference, to stick to a first inferred type, and consider subsequent variations errors, that's another thing. Normally would solve that by actually typing the variable (which wasn't possible for locals in earlier releases). Others and I have suggested a way to mark for locking the inference to first inferred type, disallowing type unions, thereby giving the result you seek. Haven't looked lately on the discussion of those issues.

With regard to a late value initialization that is still immutable in practice, that's also something that has been pondered in a couple of issues. For me it was the (seemingly straight forward) case of being able to have methods doing inits of some ivars, invoked from one or more instance initializers (the "constructors"), and there are other situations too that are even "late for real" (still undefined post object instantiation), like many come across in android dev (not related to crystal, but for example). Unfortunately compilation cost is increased a bunch by doing reachability analysis to ensure that a variable, despite being uninitialized, is never accessed before it is initialized, even though at an arbitrarily later stage. Concurrency complicates it further. _I here differentiate between "being initially nil" (which is a value, of the Nil type) and "uninitialized" meaning it is still T instead of T|Nil, but is set and subsequently used after it's containing instance life cycle has already begun_. It would be great out of a user perspective! Unfortunately I think the only way to circumvent it practically is by unsafe practices like "parking" the ivar on a dummy value before real init to avoid the Nil union - in which case the insurance of knowing it is never used before it's set (for real, not work-around-dummy-set) is thrown out of the window, along with insurance that it is not re-set.

Disclaimer: Sorry if I've completely misunderstood something and clutter up the comment space here!

RX14 commented 6 years ago

I would have it so that the value can only be written to inside the constructor (before self escapes), and after the constructor is finished the value is fixed. This also means the type is fixed for all methods. The class cannot be reentrant before self escapes, so types can be checked without if foo = @foo, and after that the variable cannot be set so the same holds. So theoretically this could work.

However I think changing the semantics so subtly there could be more confusing than helpful, and using if foo = @foo is quite succinct. I don't mind the idea of immutable instance variables but changing semantics could be a bit weird.