Overview
Data creates classes for immutable value objects with automatic implementations of equality, hash calculation, and string representation. Ruby includes Data in the core library to address common patterns where developers need simple data containers with value semantics rather than object identity.
The Data class generates methods based on the attributes provided during class definition. Each Data subclass receives automatic implementations of ==
, eql?
, hash
, inspect
, and to_s
. Pattern matching integration allows Data objects to work seamlessly with Ruby's pattern matching syntax.
# Define a Data class with two attributes
Point = Data.define(:x, :y)
# Create instances
origin = Point.new(0, 0)
point = Point.new(3, 4)
# Automatic equality comparison
origin == Point.new(0, 0) # => true
point == Point.new(3, 4) # => true
point == origin # => false
Data classes differ from Struct in their immutability and pattern matching support. While Struct creates mutable objects by default, Data objects cannot be modified after creation. This immutability ensures thread safety and prevents accidental state changes.
Person = Data.define(:name, :age)
john = Person.new("John", 30)
# Data objects are frozen
john.frozen? # => true
# No setter methods exist
john.respond_to?(:name=) # => false
Data supports inheritance, allowing subclasses to extend parent functionality while maintaining data semantics. Subclasses can define additional methods and override inherited behavior.
Basic Usage
Creating Data classes requires calling Data.define
with attribute names as symbols. The method returns a new class with the specified attributes and automatic method generation.
# Simple data class with three attributes
User = Data.define(:id, :username, :email)
# Create instances using positional arguments
user1 = User.new(1, "alice", "alice@example.com")
user2 = User.new(2, "bob", "bob@example.com")
# Access attributes through reader methods
user1.id # => 1
user1.username # => "alice"
user1.email # => "alice@example.com"
Data classes automatically implement to_h
for hash conversion, maintaining attribute order as specified during definition.
Coordinates = Data.define(:latitude, :longitude, :elevation)
location = Coordinates.new(40.7128, -74.0060, 10)
location.to_h
# => {:latitude=>40.7128, :longitude=>-74.0060, :elevation=>10}
# Convert to array maintains order
location.to_a
# => [40.7128, -74.0060, 10]
The with
method creates new instances with modified attributes while preserving immutability. This method accepts keyword arguments for the attributes to change.
Product = Data.define(:name, :price, :category)
laptop = Product.new("MacBook", 1999, "Electronics")
# Create modified copy
discounted_laptop = laptop.with(price: 1799)
laptop.price # => 1999 (unchanged)
discounted_laptop.price # => 1799 (new instance)
Data classes implement deconstruct
and deconstruct_keys
for pattern matching integration. The deconstruct
method returns attribute values as an array, while deconstruct_keys
returns a hash with specified keys.
Rectangle = Data.define(:width, :height)
rect = Rectangle.new(10, 20)
# Array destructuring
width, height = rect.deconstruct
# width => 10, height => 20
# Hash destructuring for pattern matching
rect.deconstruct_keys([:width]) # => {:width=>10}
rect.deconstruct_keys(nil) # => {:width=>10, :height=>20}
Block syntax allows defining custom methods during class creation. The block executes in the context of the new Data class.
Temperature = Data.define(:celsius) do
def fahrenheit
celsius * 9.0 / 5.0 + 32
end
def kelvin
celsius + 273.15
end
end
temp = Temperature.new(25)
temp.celsius # => 25
temp.fahrenheit # => 77.0
temp.kelvin # => 298.15
Advanced Usage
Data classes support inheritance hierarchies where subclasses extend parent attributes and functionality. Subclass definitions must include all parent attributes plus any additional ones.
# Base data class
Animal = Data.define(:name, :species)
# Subclass with additional attributes
Dog = Data.define(:name, :species, :breed, :trained) do
def bark
"#{name} says woof!"
end
end
# Inheritance chain works correctly
buddy = Dog.new("Buddy", "Canine", "Golden Retriever", true)
buddy.is_a?(Animal) # => true
buddy.name # => "Buddy"
buddy.bark # => "Buddy says woof!"
Pattern matching with Data objects supports both array and hash patterns. Array patterns match against attribute order, while hash patterns match against attribute names.
Response = Data.define(:status, :body, :headers)
success = Response.new(200, "OK", {"content-type" => "text/plain"})
error = Response.new(404, "Not Found", {})
# Array pattern matching
case success
in [200, body, *]
puts "Success: #{body}"
in [status, *, *] if status >= 400
puts "Error: #{status}"
end
# Hash pattern matching
case error
in Response(status: 404, body:)
puts "Not found: #{body}"
in Response(status: code) if code >= 500
puts "Server error: #{code}"
end
Custom equality and hash implementations can override default behavior when needed. This requires careful consideration to maintain consistency between ==
, eql?
, and hash
.
# Case-insensitive string comparison
CaseInsensitiveString = Data.define(:value) do
def ==(other)
other.is_a?(self.class) && value.downcase == other.value.downcase
end
def eql?(other)
self == other
end
def hash
value.downcase.hash ^ self.class.hash
end
end
str1 = CaseInsensitiveString.new("Hello")
str2 = CaseInsensitiveString.new("HELLO")
str1 == str2 # => true
Data classes integrate with serialization libraries through their hash conversion methods. Custom serialization logic can extend the basic to_h
implementation.
Event = Data.define(:timestamp, :type, :payload) do
def to_json(*args)
{
timestamp: timestamp.iso8601,
type: type.to_s,
payload: payload
}.to_json(*args)
end
def self.from_hash(hash)
new(
Time.parse(hash[:timestamp]),
hash[:type].to_sym,
hash[:payload]
)
end
end
event = Event.new(Time.now, :user_login, {user_id: 123})
json_string = event.to_json
restored = Event.from_hash(JSON.parse(json_string, symbolize_names: true))
Composition patterns allow Data classes to contain other Data objects while maintaining immutability throughout the object graph.
Address = Data.define(:street, :city, :postal_code)
Person = Data.define(:name, :address, :phone) do
def move_to(new_address)
with(address: new_address)
end
def update_phone(new_phone)
with(phone: new_phone)
end
end
address = Address.new("123 Main St", "Springfield", "12345")
person = Person.new("Alice", address, "555-0123")
# Create new person with different address
new_address = Address.new("456 Oak Ave", "Springfield", "12346")
moved_person = person.move_to(new_address)
person.address.street # => "123 Main St" (unchanged)
moved_person.address.street # => "456 Oak Ave" (new instance)
Common Pitfalls
Data classes freeze instances automatically, preventing modification of both the object and its attribute values. This can cause unexpected behavior when attributes contain mutable objects.
# Mutable objects within Data instances remain mutable
UserPreferences = Data.define(:settings, :tags)
prefs = UserPreferences.new({theme: "dark"}, ["ruby", "programming"])
# The Data instance is frozen
prefs.frozen? # => true
prefs.settings.frozen? # => false (Hash not frozen)
prefs.tags.frozen? # => false (Array not frozen)
# Attributes can still be mutated
prefs.settings[:theme] = "light" # Works, modifies existing hash
prefs.tags << "learning" # Works, modifies existing array
# This breaks immutability expectations
other_prefs = UserPreferences.new(prefs.settings, prefs.tags)
prefs == other_prefs # => true, but both share mutable state
The solution requires freezing mutable attribute values during instantiation or using immutable alternatives.
SafePreferences = Data.define(:settings, :tags) do
def initialize(settings, tags)
super(settings.freeze, tags.freeze)
end
end
safe_prefs = SafePreferences.new({theme: "dark"}, ["ruby"])
safe_prefs.settings.frozen? # => true
safe_prefs.tags.frozen? # => true
Pattern matching with Data classes requires understanding the relationship between attribute order and array destructuring. Changing attribute order breaks existing pattern matches.
# Original definition
Point = Data.define(:x, :y)
point = Point.new(10, 20)
case point
in [x, y]
puts "x=#{x}, y=#{y}" # Works: x=10, y=20
end
# Later changed to different order - breaks pattern matches
# Point = Data.define(:y, :x) # Don't do this
# Same pattern match now assigns y to x variable and x to y variable
Hash pattern matching avoids this issue by using attribute names instead of positions.
case point
in Point(x:, y:)
puts "x=#{x}, y=#{y}" # Robust against attribute reordering
in {x:, y:}
puts "x=#{x}, y=#{y}" # Also works, less specific
end
Inheritance hierarchies require careful attribute management. Subclasses must declare all parent attributes explicitly, and attribute order matters for pattern matching.
# Parent class
Vehicle = Data.define(:make, :model, :year)
# Subclass must include ALL parent attributes
Car = Data.define(:make, :model, :year, :doors, :fuel_type)
# This creates problems - different attribute order
# WrongCar = Data.define(:doors, :make, :model, :year, :fuel_type)
# Pattern matches expecting [make, model, year, ...] will break
Equality comparison between Data classes and other object types returns false even when attributes match. This differs from Struct behavior in some cases.
PersonData = Data.define(:name, :age)
PersonStruct = Struct.new(:name, :age)
data_person = PersonData.new("Alice", 30)
struct_person = PersonStruct.new("Alice", 30)
data_person == struct_person # => false (different classes)
data_person.to_h == struct_person.to_h # => true (same attributes)
# Only Data instances of same class compare as equal
data_person == PersonData.new("Alice", 30) # => true
Reference
Class Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Data.define(*attributes, &block) |
attributes (Symbol...), optional block |
Class |
Creates new Data class with specified attributes |
Instance Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#initialize(*values) |
values (Object...) |
Data |
Creates new instance with attribute values |
#==(other) |
other (Object) |
Boolean |
Compares equality based on class and attributes |
#eql?(other) |
other (Object) |
Boolean |
Strict equality including class and attribute types |
#hash |
None | Integer |
Hash code based on class and attribute values |
#inspect |
None | String |
Human-readable representation showing attributes |
#to_s |
None | String |
String representation (alias for inspect) |
#to_h |
None | Hash |
Hash with attribute names as keys |
#to_a |
None | Array |
Array of attribute values in definition order |
#with(**changes) |
changes (Hash) |
Data |
New instance with modified attributes |
#deconstruct |
None | Array |
Array of values for array pattern matching |
#deconstruct_keys(keys) |
keys (Array or nil) |
Hash |
Hash for hash pattern matching |
#members |
None | Array |
Array of attribute names as symbols |
#frozen? |
None | Boolean |
Always returns true (Data objects are frozen) |
Pattern Matching Patterns
Pattern Type | Syntax | Example | Description |
---|---|---|---|
Array pattern | in [a, b, c] |
in [x, y] |
Matches against deconstruct return value |
Hash pattern | in {key: var} |
in {name:, age:} |
Matches against deconstruct_keys |
Class pattern | in ClassName(attr:) |
in Point(x:, y:) |
Type-safe hash pattern matching |
Variable capture | in [a, *rest] |
in Point(x:, y: _) |
Captures or ignores values |
Inheritance Behavior
Aspect | Behavior | Example |
---|---|---|
Subclass creation | Must declare all parent attributes | Child = Data.define(:parent_attr, :child_attr) |
Method inheritance | Inherits all parent instance methods | child.parent_method works |
Pattern matching | Subclass instances match parent patterns | child_instance matches Parent(attr:) pattern |
Equality | Subclass != parent class even with same attributes | Child.new(x) != Parent.new(x) |
Common Error Types
Error | Cause | Solution |
---|---|---|
ArgumentError |
Wrong number of arguments to new |
Match argument count to defined attributes |
NoMethodError |
Calling non-existent setter | Use with method for modifications |
FrozenError |
Attempting to modify frozen object | Create new instance with with |
TypeError |
Pattern match type mismatch | Use appropriate pattern syntax |
Performance Characteristics
Operation | Time Complexity | Memory Usage | Notes |
---|---|---|---|
Instance creation | O(n) | Linear in attributes | Slightly slower than Struct |
Equality comparison | O(n) | Constant | Compares all attributes |
Hash calculation | O(n) | Constant | Based on all attributes |
Pattern matching | O(1) | Constant | Hash key lookup |
with method |
O(n) | Linear in attributes | Creates new instance |