Various ways to create structs

22 Sep 2014

The first chapter of Understanding Computation does a whirlwind tour of Ruby programming - syntax, control structures, etc. One of the constructs the author calls out is the Struct class. I've always used it like this:

Team = Struct.new(:players, :coach)
class Team
  def slogan
    "A stitch in time saves nine"
  end
end
some_team = Team.new(some_players, some_coach)

That is, I'd use the Struct constructor as a factory and assign the result to a constant. Then, if I needed to, I'd reopen the class that had just been created in order to add methods.

The example in the book does it a different way:

class Team < Struct.new(:players, :coach)
  def slogan
    "It takes one to know one"
  end
end

This creates an anonymous Struct, uses it as a superclass, assigns it to a constant, and reopens the new class all in one fell swoop. I poked around various gems and this seems like a pretty common approach - for example, Unicorn::App::Inetd::CatBody does this, and so does Arel::Attributes::Attribute.

The latter approach does result in more noise in the ancestor chain:

irb(main):001:0> Foo = Struct.new(:x) ; Foo.ancestors
=> [Foo, Struct, Enumerable, Object, Kernel, BasicObject]
irb(main):002:0> class Bar < Struct.new(:x) ; end ; Bar.ancestors
=> [Bar, #<Class:0x007ff4abb8b508>, Struct, Enumerable, Object, Kernel, BasicObject]

That's because Struct.new returns an anonymous subclass of Struct, and that subclass hasn't been assigned to a constant, so Ruby has to synthesize a to_s value from the type and object id. You could get around that using the two-arg constructor, but that creates the new class as a constant in the class Struct, which is a little weird:

irb(main):001:0> Struct.new("Bar", :x).ancestors
=> [Struct::Bar, Struct, Enumerable, Object, Kernel, BasicObject]

Sometimes I see code create Struct subclasses using the class keyword without adding methods; for example from WebSocket::Driver:

class OpenEvent < Struct.new(nil) ; end

Is there a reason to do the above rather than OpenEvent = Struct.new(nil)?

Edit: Bruno Michel noted that the Struct constructor also accepts a block:

Foo = Struct.new(:x) do
  def hello
    "hi"
  end
end

This is a nice technique because it results in a clean ancestor chain.

Edit 2: Tom Stuart (the author of 'Understanding Computation') pointed out this great writeup on Structs by James Edward Grey II where he explains why he's using Structs in the way he does. Definitely worth a read, especially with Ara Howard weighing in with a comment about his Map implementation.

Thanks to Thomas Olausson for reviewing this post!