

OpenStruct in Ruby
In this article, we’re going to explore the following topics:
- the
OpenStructclass - data structure: creation and manipulation
OpenStructand lazy loading- data structures behind the scene
Feel free to read the Struct in Ruby article if you are unfamiliar with data structures in Ruby. This will give you a better comprehension of the technical notions of this article.The OpenStruct class
The OpenStruct class allows you to create flexible data structures.
In effect, the structure doesn’t need to provide a rigid list of members as it doesn’t define any structure type but data structures that you fill in afterwards.
It provides a pair of getter/setter methods for each attribute that it contains. This is similar to the attr_accessor method for classes.
Feel free to read theAttributes in Rubyarticle if you are unfamiliar with theattr_*methods in Ruby.
This class is part of the Ruby’s Standard Library (a.k.a the stdlib).
Let’s have a look to its ancestor chain
irb> require 'ostruct'
=> true
irb> OpenStruct.ancestors
=> [OpenStruct, Object, Kernel, BasicObject]
As it’s not part of the Ruby Core, we have to require the ostruct library which contains the OpenStruct class definition.
The OpenStruct inherits from the default parent class Object.
Note that it doesn’t include any module.
In effect, unlike the Struct class, it doesn’t include the Enumerable module.
Feel free to read theRuby Object Modelarticle if you are unfamiliar with theObjectclass and the ancestor chain.
Feel free to read theThe Enumerable module in Ruby: Part Iarticle if you are unfamiliar with theEnumerablemodule.
Data structure: creation and manipulation
The OpenStruct::new is in charge of creating new data structure.
This method accepts a Hash, a Struct or an OpenStruct as parameter
Firstly, we instantiate a new OpenStruct object with one attribute called ram and we store this object in the computer variable.
Then we access the ram attribute by 3 different ways:
computer.ram: theramaccessor methodcomputer[:ram]: theOpenStruct#[]with asymbolkeycomputer['ram']: theOpenStruct#[]with astringkey
Then we define the screens attribute.
Notice that there is 3 ways to define a new attribute or to modify the value of an existing one:
computer.screens=: thescreens=accessor methodcomputer[:screens]=: theOpenStruct#[]=with asymbolkeycomputer['screens']=: theOpenStruct#[]=with astringkey
Then we access the value of the screens attribute the same way as the ram attribute.
Unlike the Struct class that defines structure type with a rigid attributes list, the OpenStruct class defines new data structures that can add attributes on-the-fly, out of the data structure definition scope — like the screen attribute in the above example.
OpenStruct and lazy loading
In order to save memory and speed up the access to an attribute, the accessor methods of an attribute are lazy loaded at certain points.
This means that the methods are defined only when a bunch of defined actions are triggered.
This allows your program to only define the minimum amount of methods that are required to make your data structure works.
So let’s have a look to how it works
At the OpenStruct initialization, we see that the computer.ram and computer.ram= accessor methods are not defined yet.
Then we see that after a call to computer.ram, the computer.ram and computer.ram= methods are defined.
Note that a call to computer[:ram] doesn’t define the accessor methods.
Then we call the computer[:screens] = 2 method.
After this method call, the computer.screens and computer.screens= accessors are defined.
To recap, accessor methods are defined only when:
- an accessor method of an existing (or not) attribute is called for the first time.
- the
OpenStruct#[]=method is called for the first time —computer[:screens] = 2for example.
Data structures behind the scene
Now that we are more familiar with the OpenStruct class and the data structures, let’s dive into what happens behind the scene when we create and manipulate an OpenStruct object.
Each data structure from an OpenStruct defines an internal hash that contains a table of correspondence between each attribute and their value.
This container is commonly called the table
Note that each attribute name is converted to a Symbol before to be included into the table.
Now let’s have a look to the evolution of the computer ‘s table
Here the table of the computer’s data structure is {ram: "4GB"}.
Now let’s add the screens attribute to the computer object
At this point the table is {ram: "4GB", screens: 2}.
What happens when an attribute that doesn’t exist is called?
For example, a call to the computer.cores= setter method
When a method is not defined within the entire ancestor chain of a given object then Ruby automatically calls the first defined method_missing Hook Method in this ancestor chain.
The OpenStruct defines its own version of the method_missing method.
So when computer.cores = 2 is called then the OpenStruct#method_missing method is called behind the scene.
Let’s detail the execution flow of this method when the computer.cores = 2 is called:
- 1/ getter and setter methods are defined and the key passed as parameter is converted into a
Symbol— in our case:cores - 2/ the value is inserted into the internal hash using this key —
@table[:cores] = 2 - 3/ the value is returned —
2in our case
This execution flow is slight the same for a call to computer[:cores] = 2 and computer.cores.
Voilà !
Thank you for taking the time to read this post :-)
Feel free to 👏 and share this article if it has been useful for you. 🚀
Here is a link to my last article: Struct in Ruby.