DEV Community

Kelly Popko
Kelly Popko

Posted on • Edited on

Ruby Data Class vs Struct

This is part 2 in this series on Ruby's Data Class. Part 1 covered Syntax and part 3 explores immutability workarounds with Data.

In practice, a main difference between instances of Data and Struct is mutability:

  • Instances of Data are immutable*
  • Instances of Struct are mutable

*Mutable values passed to instances of Data remain immutable by default.

Data also has fewer (and different) 'built-in' methods than Struct.

This post explores differences in syntax and behavior between Data and Struct.

Syntax comparison: Create New Instance

Data

House = Data.define(:rooms, :area, :floors)
ranch = House.new(rooms: 5, area: 1200, floors: 1)
# => #<data House rooms=5, area=1200, floors=1>
Enter fullscreen mode Exit fullscreen mode

Struct

House = Struct.new(:rooms, :area, :floors, keyword_init: true)
ranch = House.new(rooms: 5, area: 1200, floors: 1)
# => #<struct House rooms=5, area=1200, floors=1>
Enter fullscreen mode Exit fullscreen mode

Mutability: Attempt to Renovate the house...

Data

House = Data.define(:rooms, :area, :floors)
ranch = House.new(rooms: 5, area: 1200, floors: 1)
# => #<data House rooms=5, area=1200, floors=1>

ranch.floors = 2
# (irb):3:in `<main>': undefined method `floors=' for an instance of House (NoMethodError)
Enter fullscreen mode Exit fullscreen mode

This tells us there is no 'writer' (sometimes called "setter" in other
languages) method to use.

If we try to define a writer method on House and update that value, FrozenError is raised.

House = Data.define(:rooms, :area, :floors) do
  def floors=(quantity)
    @floors = quantity
  end
end
# => House

cottage = House.new(rooms: 5, area: 700, floors: 1)
# => #<data House rooms=5, area=700, floors=1>

cottage.floors = 2
# (irb):21:in `floors=': can't modify frozen House: #<data House rooms=5, area=700, floors=1> (FrozenError)
cottage.frozen?
# => true
Enter fullscreen mode Exit fullscreen mode

What we can do with a Data object is clone it and update any of the attribute values.

House = Data.define(:rooms, :area, :floors)
cottage = House.new(rooms: 5, area: 1200, floors: 1)
# => #<data House rooms=5, area=1200, floors=1>

two_story_cottage = cottage.with(floors: 2)
# => #<data House rooms=5, area=1200, floors=2>
Enter fullscreen mode Exit fullscreen mode

Struct

With Struct, we can renovate.

House = Struct.new(:rooms, :area, :floors, keyword_init: true)
ranch = House.new(rooms: 5, area: 1200, floors: 1)
# => #<struct House rooms=5, area=1200, floors=1>

ranch.floors = 2
# => 2

ranch
# => #<struct House rooms=5, area=1200, floors=2>

ranch.frozen?
# => false
Enter fullscreen mode Exit fullscreen mode

Built-in Methods

Data Class

>> Data.methods(false)
=> [:define]
>> Data.instance_methods(false)
=>
[:deconstruct_keys,
 :pretty_print,
 :pretty_print_cycle,
 :deconstruct,
 :hash,
 :==,
 :inspect,
 :members,
 :to_s,
 :with,
 :eql?,
 :to_h]
Enter fullscreen mode Exit fullscreen mode

Struct

>> Struct.methods(false)
=> [:new]
>> Struct.instance_methods(false)
=>
[:deconstruct_keys,
 :==,
 :members,
 :to_a,
 :to_s,
 :[],
 :[]=,
 :values_at,
 :eql?,
 :to_h,
 :select,
 :filter,
 :pretty_print,
 :pretty_print_cycle,
 :deconstruct,
 :hash,
 :inspect,
 :each_pair,
 :values,
 :length,
 :dig,
 :size,
 :each]
Enter fullscreen mode Exit fullscreen mode

Comparison

>> struct_methods - data_methods
=>
[:to_a,
 :[],
 :[]=,
 :values_at,
 :select,
 :filter,
 :each_pair,
 :values,
 :length,
 :dig,
 :size,
 :each]

>> data_methods - struct_methods
=> [:with]
Enter fullscreen mode Exit fullscreen mode

Top comments (0)