Ruby - Immutable Objects

How to test whether a Ruby object is immutable?

There are no primitive objects in Ruby. This can therefore not be detected in a straightforward manner.

Can't you simply use Marshal or YAML for your versioned store? Then you'll get loading and saving of all object types for free. Why reinvent the wheel?

I don't know what you want to achieve exactly, but looking at the source of YAML may be interesting to see how they handle this problem. The Ruby YAML encoding implementation simply implements the to_yaml method for all relevant classes. See yaml/rubytypes.rb.

Ruby: Why freeze mutable objects assigned to constants?

You should freeze the value assigned to IP because you've declared IP to be a constant. This indicates that you don't want the value assigned to IP to be mutated.

The problem is that in ruby, assigning a value to a constant does not make the value immutable. You just get a warning if you mutate the value assigned to the constant. To make the value actually immutable, you need to .freeze the value assigned to the constant. After you've frozen a value assigned to a constant, if you try to change the value, you will hit a runtime error.

What is the difference between being immutable and the fact that there can only be one instance of a Symbol?

Your sentence is fine; you're not sure of the common phrase used to describe a class with only one instance. I'll explain that as I go along.


An object that is immutable cannot change through any operations done on it. This means that any operation that would change a symbol would generate a new one instead.

:foo.object_id # 1520028
:foo.upcase.object_id # 70209716662240
:foo.capitalize.object_id # 70209719120060

You can certainly write objects that are immutable, or make them immutable (with some caveats) via freeze, but you can always create a new instance of them.

f = "foo"
f.freeze
f1 = "foo"
puts f.object_id == f1.object_id # false

An object that only ever has one instance of itself is considered to be a singleton.

  • If there's only one instance of it, then you only store it in memory once.
  • If you attempt to create it, you only get the previously existing object back.

Are strings in Ruby mutable?

Yes, strings in Ruby, unlike in Python, are mutable.

s += "hello" is not appending "hello" to s - an entirely new string object gets created. To append to a string 'in place', use <<, like in:

s = "hello"
s << " world"
s # hello world

Ruby immutability of strings and symbols (What if we store them in variables)

Ruby variables are references to objects, so when you send a method to a variable, the object it references is the context in which it is evaluated. It's probably more clear to look at the first image in the top rated answer (below the accepted answer) here.

So, to figure out what's going on, let's dig into the documentation a bit and see what happens with your code snippet.

Ruby's Symbol class documentation:
https://ruby-doc.org/core-2.5.0/Symbol.html

Symbol objects represent names and some strings inside the Ruby interpreter. They are generated using the :name and :"string" literals syntax, and by the various to_sym methods. The same Symbol object will be created for a given name or string for the duration of a program's execution, regardless of the context or meaning of that name. Thus if Fred is a constant in one context, a method in another, and a class in a third, the Symbol :Fred will be the same object in all three contexts.

Ruby's Object#object_id documentation:
https://ruby-doc.org/core-2.5.1/Object.html#method-i-object_id

Returns an integer identifier for obj.

The same number will be returned on all calls to object_id for a given object, and no two active objects will share an id.

So here's what's happening step-by-step:

# We create two variables that refer to the same object, :foo
var1 = :foo
var2 = :foo

var1.object_id = 2598748
var2.object_id = 2598748
# Evaluated as:
# var1.object_id => :foo.object_id => 2598748
# var2.object_id => :foo.object_id => 2598748

As discussed in the first link above, Ruby is pass-by-value, but every value is an Object, so your variables both evaluate to the same value. Since every symbol made of the same string ("foo" in this case) refers to the same object, and Object#object_id always returns the same id for the same object, you get the same id back.

Ruby CONSTANTS seem to be INVISIBLY ALTERABLE?

TL;DR

Short of monkey-patching Kernel#warn (see https://stackoverflow.com/a/662436/1301972) to raise an exception, you won't be able to prevent reassignment to the constant itself. This is generally not a pragmatic concern in idiomatic Ruby code where one expects to be able to do things like reopen classes, even though class names are also constants.

A Ruby constant isn't actually immutable, and you can't freeze a variable. However, you can get an exception to be raised when something attempts to modify the contents of a frozen object referenced by the constant.

Freezing Objects Deeply with Plain Ruby

Freezing an Array is easy:

CONSTANT_ONE = %w[one two three].freeze

but the strings stored in this Array are really references to String objects. So, while you can't modify this Array, you can still (for example) modify the String object referenced by index 0. To solve this problem, you need to freeze not just the Array, but the objects it holds, too. For example:

CONSTANT = %w[one two three].map(&:freeze).freeze

CONSTANT[2] << 'four'
# RuntimeError: can't modify frozen String

CONSTANT << 'five'
# RuntimeError: can't modify frozen Array

Freezing Objects Recursively with a Gem

Since freezing recursive references can be a bit unwieldy, it's good to know there's a gem for that. You can use ice_nine to deep-freeze most objects:

require 'ice_nine'
require 'ice_nine/core_ext/object'

OTHER_CONST = %w[a b c]
OTHER_CONST.deep_freeze

OTHER_CONST << 'd'
# RuntimeError: can't modify frozen Array

OTHER_CONST[2] = 'z'
# RuntimeError: can't modify frozen Array

A Better Way to Use Ruby Constants

Another option to consider is calling Object#dup when assigning the value of a constant to another variable, such as instance variables in your class initializers, in order to ensure you don't mutate your constant's references by accident. For example:

class Foo
CONSTANT = 'foo'
attr_accessor :variable

def initialize
@variable = CONSTANT.dup
end
end

foo = Foo.new
foo.variable << 'bar'
#=> "foobar"

Foo::CONSTANT
#=> "foo"


Related Topics



Leave a reply



Submit