Skip to content

Instantly share code, notes, and snippets.

@ybiquitous
Last active May 29, 2026 04:45
Show Gist options
  • Select an option

  • Save ybiquitous/5d823fa52dfaa9a458293ba271ccbc71 to your computer and use it in GitHub Desktop.

Select an option

Save ybiquitous/5d823fa52dfaa9a458293ba271ccbc71 to your computer and use it in GitHub Desktop.
Proposal: Add Module#class_reader to Ruby
class Module
def class_reader(name, value)
define_singleton_method(name) { value }
end
end
# === Test ===
if ARGV.delete('--test')
require 'minitest/autorun'
class ClassReaderTest < Minitest::Test
class Foo
class_reader :default_list, ['foo'].map(&:to_sym).freeze
end
class Bar < Foo; end
# Workaround by class ivar
class Foo
@default_list_workaround = ['foo'].map(&:to_sym).freeze
def self.default_list_workaround = @default_list_workaround
end
# Just using `define_singleton_method` does not cache a passed value
class Foo
define_singleton_method(:default_list_no_cache) { ['foo'].map(&:to_sym).freeze }
end
def test_class_reader
assert_equal [:foo], Foo.default_list
assert_same Foo.default_list, Foo.default_list
end
def test_subclass
assert_equal [:foo], Bar.default_list
assert_same Bar.default_list, Foo.default_list
end
def test_workaround_by_class_ivar
assert_equal [:foo], Foo.default_list_workaround
assert_same Foo.default_list_workaround, Foo.default_list_workaround
end
def test_define_singleton_method_not_cache
assert_equal [:foo], Foo.default_list_no_cache
refute_same Foo.default_list_no_cache, Foo.default_list_no_cache
end
end
end
@ybiquitous

Copy link
Copy Markdown
Author

class_reader can resolve the problems:

# Allocates a new object on every call
def self.default_list = [:foo]

# Thread-unsafe due to ||=
def self.default_list = (@default_list ||= [:foo].freeze)

@ybiquitous

Copy link
Copy Markdown
Author

Prior art: Active Support's Class#class_attribute.

However, I don't want mutation, so I'm excluding ideas, such as class_writer and class_accessor.

@ybiquitous

Copy link
Copy Markdown
Author

Another prior art: Active Support's Module#mattr_reader.

This one is closer to what I want, but the definition is a bit longer due to the more options, and the name (mattr_reader) doesn't seem intuitive.

class Foo
  mattr_reader :default_list, default: [:foo].freeze, instance_reader: false
end

@ybiquitous

ybiquitous commented May 29, 2026 •

Copy link
Copy Markdown
Author

Alternative names to class_reader:

  • singleton_reader
  • const_reader
  • static_reader
  • class_attr_reader
  • class_constant_method
  • define_class_constant
  • class_value
  • class_constant
  • cattr_reader/mattr_reader (from Active Support)
  • etc.

@ybiquitous

ybiquitous commented May 29, 2026 •

Copy link
Copy Markdown
Author

What is the difference between class_reader and define_singleton_method?

class_reader :default_list, [:foo].freeze

[:foo].freeze is captured in the closure using the value second argument, so it's evaluated once.


define_singleton_method(:default_list) { [:foo].freeze }

[:foo].freeze in the block is repeatedly evaluated once the default_list method is called.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment