Working with Ruby
Hi, I am Jan. This is my old Ruby blog. I still post about Ruby, but I now do it on idiosyncratic-ruby.com. You should also install Irbtools to improve your IRB.

value_struct: Read-only structs in Ruby

Ruby’s structs are one of my favorite data types in Ruby. They help you to keep some defined structure in the dynamic world of Ruby. Often, it makes sense to use them instead of hashes or arrays. Read-only structs take the idea a level further.

Ruby’s Struct class

The Struct class is available in the Ruby core library, which means, you don’t have to require it, it’s just there:

Point = Struct.new :x, :y
# Point is now a new Ruby class that has getters and setters for x and y.

p = Point.new(1,2)
# Creates an instance

p.x     #=> 1
p.x = 0 #=> 0

You can also subclass these struct classes or just pass a block in which you define additional methods for your struct class. Structs are implemented in C and are usually faster than hashes. The values get stored in special instance variables that you cannot access from Ruby land, instead, you have to use the getter and setter methods. You can read more about structs in these resources.

Immutable structs

Having immutable data structures is a matter of taste. However, you get the following advantages:

  • It’s a concept/aesthetic. You can rely that nobody will ever change the value of a field
  • Immutable structs can be thought of as value objects: Small objects that represent a specific kind of data
  • Thread safeness

Value structs

value_struct is a small implementation of immutable structs. They behave like structs, except for the last statement in the previous example: They don’t have setters:

require 'value_struct'

Point = ValueStruct.new :x, :y
p = Point.new(1,2)
p.x     # => 1
p.x = 0 # NoMethodError

Implementation

The implementation is pretty simple. The first version of the gem looked like that:

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ValueStruct < Struct
  VERSION = '0.2.0'

  undef []=

  def initialize(*args, &block)
    super(*args, &block)
    members.each{ |member| instance_eval{ undef :"#{member}=" } }
  end

  def inspect
    super.to_s.sub('struct', 'ValueStruct')
  end
end

However, the current code differs for easy extendibility and performance.

Benchmarks

Usually, benchmarks don’t play a big role in the Ruby community, however, for such a basic class, which is intended to be used for a lot of objects, little overheads can get noticeable. That’s why I did a benchmark that just created many structs and accessed their fields:

              Struct:   2.490000   0.000000   2.490000 (  2.506390)
               Value:  11.110000   0.010000  11.120000 ( 11.161834)
     ImmutableStruct:   3.360000   0.020000   3.380000 (  3.396900)
         ValueStruct:   2.560000   0.020000   2.580000 (  2.590096)
              [Hash]:   3.710000   0.010000   3.720000 (  3.733843)

You can find the benchmark file at spec/benchmark.rb

Mixin API

The value_struct gem provides optional mixins that provide useful functionality for working with (value) structs. For example, you can extend the #dup method to take a hash as parameter for directly changing values in the new copy. Other mixins include a #to_h method(which will come in 2.0 anyway), auto freezing of new instances or a strict check if new instances get initialized with the correct amount of arguments.

Use (value) structs now!

Normal structs are already available in your Ruby environment.

To get value structs, visit the github page and do:

$ gem install value_struct

Other gems, doing something similar

Creative Commons License