immutable = deeply frozen
- TrueClass
- FalseClass
- NilClass
- Integer
- Float
- Rational
- Complex
- Encoding
- Symbol
- frozen Strings, both from
# frozen_string_literal: true
and from-str
. - Regexp (only literals on CRuby, all on TruffleRuby)
- Range (only Range instances, not subclass instances)
- Process::Status
Should be immutable but not yet on CRuby:
- Thread::Backtrace::Location
- MatchData (the referenced string is a frozen copy)
- BigDecimal
- probably more classes, harder to get a list
Methods:
.allocate
=> TypeError (allocator undefined for MyClass), or.allocate
is undefined on that class (Complex, Rational, MatchData).new
=> typically undefined and there are other methods or there is a literal notation to create instances#initialize
=>BasicObject#initialize
, noop, takes 0 arguments#initialize_copy
=>Kernel#initialize_copy
, which for frozen objects raises FrozenError (can't modify frozen MyClass: value)#clone
=>Kernel#clone
returns the receiver because it knows it's an immutable object#dup
=>Kernel#dup
returns the receiver because it knows it's an immutable object#freeze
=>Kernel#freeze
, noop since already frozen#frozen
=>Kernel#frozen?
, true since creation
Often no .new
but instead literal notation or Kernel#MyClass(args) method.
Note that there is Numeric#clone
and Numeric#dup
, but they are not enough (Kernel.instance_method(:clone).bind_call(1) => 1
)
because it knows it's an immutable object
is currently an hardcoded list, but ideally non-core types should also be able to be recognized as immutable.
That check doesn't work for BigDecimal for instance:
Kernel.instance_method(:clone).bind_call(BigDecimal(0)) # => allocator undefined for BigDecimal (TypeError)
vs BigDecimal(0).clone # => itself
.
Advantages:
- No need to worry about .allocate-d but not #initialize-d objects => not need to check in every method if the object is #initialize-d
- internal state/fields can be truly
final
/const
. - simpler and faster 1-step allocation since there is no dynamic call to #initialize (instead of .new calls alloc_func and #initialize)
- Known immutable by construction, no need for extra checks, no need to iterate instance variables since no instance variables
- Potentially lower footprint due to no instance variables
- Can be shared between Ractors freely and with no cost
- Can be shared between different Ruby execution contexts in the same process and even in persisted JIT'd code
- Easier to reason about both for implementers and users since there is no state
- Can be freely cached as it will never change
The immutable classes above +
- TracePoint, Binding, Proc, Method, UnboundMethod
- Ractor, Ractor::MovedObject
- Thread::Backtrace::Location, MatchData (both should be immutable instead).
I think the following should be added to that list:
- Module, Class (simplifies a lot in Ruby implementations and removes many "is initialized" checks, also known superclass from the start).
- Enumerator::ArithmeticSequence, Thread (they currently have a custom
#initialize
even though no allocator) - Enumerator (currently defined allocator and
#initialize
) forEnumerator.new {}
, but could defineEnumerator.new
to avoid the need. - Random
- Probably more
Advantages:
- No need to worry about .allocate-d but not #initialize-d objects => not need to check in every method if the object is #initialize-d
- internal state/fields can be truly
final
/const
, except for ivars. - simpler and faster 1-step allocation since there is no dynamic call to
#initialize
(instead of .new calls alloc_func and #initialize)
These are currently all not #frozen?
as they exhibit mutable behavior through method calls.
Some could be shallow-frozen, i.e., they would refer to mutable objects, but the instance itself always refer to the given objects, and they would have no mutable fields and not support ivars. They would be marked as #frozen?
on creation.
That could make sense for Proc, Method, UnboundMethod (those have no mutating methods).
$ ruby --disable-gems -e 'ObjectSpace.each_object(Class) { |c| p c if !c.respond_to?(:allocate) }'
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux]
Complex
Rational
MatchData
$ ruby --disable-gems -e 'ObjectSpace.each_object(Class) { |c| begin; c.allocate; rescue TypeError => e; p [c,e]; end if c.respond_to?(:allocate) && !c.singleton_class? }' | sort
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux]
[Binding, #<TypeError: allocator undefined for Binding>]
[Encoding, #<TypeError: allocator undefined for Encoding>]
[Enumerator::ArithmeticSequence, #<TypeError: allocator undefined for Enumerator::ArithmeticSequence>]
[FalseClass, #<TypeError: allocator undefined for FalseClass>]
[Float, #<TypeError: allocator undefined for Float>]
[Integer, #<TypeError: allocator undefined for Integer>]
[Method, #<TypeError: allocator undefined for Method>]
[NilClass, #<TypeError: allocator undefined for NilClass>]
[Process::Waiter, #<TypeError: allocator undefined for Process::Waiter>]
[Proc, #<TypeError: allocator undefined for Proc>]
[Ractor::MovedObject, #<TypeError: allocator undefined for Ractor::MovedObject>]
[Ractor, #<TypeError: allocator undefined for Ractor>]
[Random::Base, #<TypeError: allocator undefined for Random::Base>]
[RubyVM::AbstractSyntaxTree::Node, #<TypeError: allocator undefined for RubyVM::AbstractSyntaxTree::Node>]
[RubyVM::InstructionSequence, #<TypeError: allocator undefined for RubyVM::InstructionSequence>]
[RubyVM, #<TypeError: allocator undefined for RubyVM>]
[Struct, #<TypeError: allocator undefined for Struct>]
[Symbol, #<TypeError: allocator undefined for Symbol>]
[Thread::Backtrace::Location, #<TypeError: allocator undefined for Thread::Backtrace::Location>]
[Thread, #<TypeError: allocator undefined for Thread>]
[TracePoint, #<TypeError: allocator undefined for TracePoint>]
[TrueClass, #<TypeError: allocator undefined for TrueClass>]
[UnboundMethod, #<TypeError: allocator undefined for UnboundMethod>]
[RubyVM::AbstractSyntaxTree::Node, TracePoint, Complex, Rational, Process::Waiter, RubyVM::InstructionSequence, Thread::Backtrace::Location, Thread, RubyVM, Ractor::MovedObject, Ractor, Enumerator::ArithmeticSequence, Binding, UnboundMethod, Method, Proc, Random::Base, MatchData, Struct, Float, Integer, Symbol, Encoding, FalseClass, TrueClass, NilClass]
#initialize:
BasicObject#initialize: [RubyVM::AbstractSyntaxTree::Node, TracePoint, Complex, Rational, RubyVM::InstructionSequence, Thread::Backtrace::Location, RubyVM, Ractor::MovedObject, Ractor, Binding, UnboundMethod, Method, Proc, MatchData, Float, Integer, Symbol, Encoding, FalseClass, TrueClass, NilClass]
Custom #initialize: [Process::Waiter, Thread, Enumerator::ArithmeticSequence, Random::Base, Struct]