Skip to content

Instantly share code, notes, and snippets.

@JEG2
Created June 3, 2013 21:50
Show Gist options
  • Save JEG2/5701747 to your computer and use it in GitHub Desktop.
Save JEG2/5701747 to your computer and use it in GitHub Desktop.
Thinking out loud about the merits and drawbacks of two different usages of Struct…

How Should We Use Struct?

The Choice

It's common in Ruby to see some code setup a Struct like this:

class Specialized < Struct.new(:whatever)
  # ... define custom methods here...
end

Struct supports this kind of modification using a block though, so the above could be written as:

Specialized = Struct.new(:whatever) do
  # ... define custom methods here...
end

Those are the possibilities.

My Opinion

There's no right or wrong way to handle this. However, I do prefer the second form. I'm going to try to make a case for why that is.

I have two main reasons for favoring the second form.

First, I find the second style conceptually easier to clarify. To me, the first form is kind of heavyweight, concept-wise. You need to understand that Struct.new() returns a Class and that the parent class in a class definition can be an arbitrary Ruby expression.

I think it gets even tougher to understand when you eventually run into the problem of combining this style with Ruby's open classes or some autoloading mechanism. When someone comes to me and asks, "Why am I getting a parent class mismatch error?" Explaining that involves more twists. Well, you see, you didn't just customize a Struct. You built a Struct and then defined a subclass of that. Because that Struct is dynamically constructed, you get a different parent class anytime Ruby evaluates that expression, and Ruby doesn't allow the same subclass to reference different parent classes. Yuck. I don't even like describing this complexity and it's sort of reflected in the code by the anonymous Class:

=> [MyStruct,
    #<Class:0x007f8ba7389d18>,
    Struct,
    Enumerable,
    Object,
    PP::ObjectMixin,
    Kernel,
    BasicObject]

In contrast, I feel like describing the second form is about as simple as "… and the constructor can take a block for customizing what is created, say by defining new methods in it."

The second thing I prefer about the block form is a concept I've become aware of thanks to Josh Susser: code malleability. Consider that you begin with a trivial Struct:

Trivial = Struct.new(:whatever)

Now, if I later need to customize it, I can either just tack a block onto the end of the call or begin restructuring the statement. Then what if I decide to back out the customizations? Again, I can restructure or just remove the block.

In other words, the block form flows naturally into and out of the normal Struct usage. It's more malleable. This means refactoring is easier and you are more likely do it when needed.

There's another kind of related point here. Normal Struct usage has a similar form to block Struct usage. This visual similarity gives me the hint, "Oh, this is just a Struct that I am dealing with."

Again, these are just my opinions.

@glv
Copy link

glv commented Jun 11, 2013

To answer @tenderlove's comment in https://twitter.com/tenderlove/status/344584310222880768, it's a shame Struct doesn't have something like this (ugly and quickly thrown together, but you get the idea):

class Struct
  def method_missing(meth, *args)
    if members.include?(meth) && args.empty?
      self[meth]
    elsif meth.to_s =~ /^(\w)=$/ && members.include($1.to_sym) && args.size == 1
      self[$1.to_sym] = args.first
    else
      super
    end
  end
end

@ahoward
Copy link

ahoward commented Jun 11, 2013

@glv

require 'map'

m = Map.struct

m.foo = :bar

p m.foo #=> :bar

https://github.com/ahoward/map/blob/master/lib/map/struct.rb#L12

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment