Project

General

Profile

Actions

Feature #18035

open

Introduce general model/semantic for immutable by default.

Added by ioquatix (Samuel Williams) 2 months ago. Updated about 1 month ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:104560]

Description

It would be good to establish some rules around mutability, immutability, frozen, and deep frozen in Ruby.

I see time and time again, incorrect assumptions about how this works in production code. Constants that aren't really constant, people using #freeze incorrectly, etc.

I don't have any particular preference but:

  • We should establish consistent patterns where possible, e.g.
    • Objects created by new are mutable.
    • Objects created by literal are immutable.

We have problems with how freeze works on composite data types, e.g. Hash#freeze does not impact children keys/values, same for Array. Do we need to introduce freeze(true) or #deep_freeze or some other method?

Because of this, frozen does not necessarily correspond to immutable. This is an issue which causes real world problems.

I also propose to codify this where possible, in terms of "this class of object is immutable" should be enforced by the language/runtime, e.g.

module Immutable
  def new(...)
    super.freeze
  end
end

class MyImmutableObject
  extend Immutable

  def initialize(x)
    @x = x
  end

  def freeze
    return self if frozen?

    @x.freeze

    super
  end
end

o = MyImmutableObject.new([1, 2, 3])
puts o.frozen?

Finally, this area has an impact to thread and fiber safe programming, so it is becoming more relevant and I believe that the current approach which is rather adhoc is insufficient.

I know that it's non-trivial to retrofit existing code, but maybe it can be done via magic comment, etc, which we already did for frozen string literals.

Updated by ioquatix (Samuel Williams) 2 months ago

  • Subject changed from Introduce general module for immutable by default. to Introduce general model/semantic for immutable by default.

Fix title.

Updated by duerst (Martin Dürst) 2 months ago

This is mostly just a generic comment that may not be very helpful, but I can only say that I fully agree. Even before talking about parallel stuff (thread/fiber), knowing some object is frozen can be of help when optimizing.

One thing that might be of interest is that in a method chain, a lot of the intermediate objects (e.g. arrays, hashes) may be taken to be immutable because they are just passed to the next method in the chain and never used otherwise (but in this case, it's just the top object that's immutable, not necessarily the components, which may be passed along the chain).

Updated by ioquatix (Samuel Williams) 2 months ago

Regarding method chains, one thing that's always bothered me a bit is this:

def foo(*arguments)
    pp object_id: arguments.object_id, frozen: arguments.frozen?
end

arguments = [1, 2, 3].freeze
pp object_id: arguments.object_id, frozen: arguments.frozen?

foo(*arguments)

I know it's hard to implement this and also retain compatibility, but I feel like the vast majority of allocations done by this style of code are almost always unneeded. We either need some form of escape/mutation analysis, or copy-on-write for Array/Hash (maybe we have it already and I don't know).

Actions #4

Updated by jeremyevans0 (Jeremy Evans) 2 months ago

  • Backport deleted (2.6: UNKNOWN, 2.7: UNKNOWN, 3.0: UNKNOWN)
  • Tracker changed from Bug to Feature
Actions #5

Updated by Eregon (Benoit Daloze) about 1 month ago

  • Description updated (diff)

Updated by Eregon (Benoit Daloze) about 1 month ago

Many things discussed in the description here.

I think it's important to differentiate shallow frozen (Kernel#frozen?) and deep frozen (= immutable), and not try to change their meaning.
So for example overriding freeze to deep freeze does not seem good.

There was a suggestion for deep_freeze in #17145, which IMHO would be a good addition.

Objects created by literal are immutable.

I don't agree, for instance [] and {} should not be frozen, that would just be counter-productive in many cases.

Maybe CONSTANT = value should .deep_freeze the value, this was discussed with Ractor.make_shareable but that was rejected (#17273).

There is also the question of how to mark a class as creating immutable objects.
And potentially still allow to subclass it, and what it should do with initialize_copy, allocate, etc.
That's illustrated with the Immutable above but otherwise not much discussed.
I think that's probably worth its own ticket, because it's a big enough subject of its own, I'll try to make one.

copy-on-write for Array

That's required for efficient Array#shift so you can assume it's there on all major Ruby implementations.

Actions

Also available in: Atom PDF