Data.define in Ruby 3.2: An Introduction to Immutability

Ruby 3.2 introduces a new Data class to define immutable value objects that contain a simple set of values. Similar to the Struct class in Ruby, it provides a simpler and more restricted API. In this article, we’ll explore what the Data class is and how it can be useful from a practical perspective.

You may find this concept familiar because it resembles Struct.new, which also defines similar classes. However, there is a fundamental difference. Data classes are immutable, meaning they lack setters for the attributes.

Creating a Data Class

To create a Data class, you must define a class name and a list of attributes. You can do this using the Data.define method

1
Country = Data.define(:name, :currency)

With that, you can create a new instance of the Country class by passing values to the attributes:

1
country = Country.new("Peru", "Sol")

You can access the attributes of a Data class instance using the accessor methods.

1
2
country.name # => "Peru"
country.currency # => "Sol"

Immutability

Immutability refers to the property of an object that cannot be modified once created. This characteristic brings several benefits, including easier reasoning about code behavior and avoiding unexpected side effects. If you try to do so, an exception will be raised.

1
country.name = "Argentina" # => RuntimeError: can't modify frozen Data

Composition

You can use the Data class to create new classes that represent more complex objects. For example, you can compose multiple Data classes together to create a new class with a combination of attributes.

1
2
3
4
5
6
7
8
9
10
11
Food = Data.define(:name, :main_ingredient, :vegetarian)
# => Food

food = Food.new("Ceviche", "Fish", false)
# => #<data Food name="Ceviche", main_ingredient="Fish", vegetarian=false>

Country = Data.define(:name, :currency, :food)
# => Country

country = Country.new("Peru", "Sol", food)
# => #<data Country name="Peru", currency="Sol", food=#<data Food name="Ceviche", main_ingredient="Fish", vegetarian=false>

For details, check the merge here: https://github.com/ruby/ruby/pull/6353