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.
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.
Thanks James, this is really interesting. I agree with all of it!
Like you, I use
Struct
’s block syntax (or just plain assignment) wherever practical in my day-to-day code. For Understanding Computation I tried it both ways, but ultimately went with class definitions for a few reasons:Struct.new
, but I don’t call it out, so hopefully newbies won’t find it distracting. I pretty much just announce “this is the syntax for declaring aStruct
” and leave it at that. (I don’t even mention instance variables anywhere.) I’m taking a gamble here that it’s okay to completely ignore the detail of howclass Foo < Struct.new(:bar)
works without compromising one’s understanding of the subsequent material.class
. I felt that this would provoke irrelevant (albeit interesting) questions in the mind of a curious reader — I imagined them deriving the existence ofmodule_eval
from first principles — so I went withclass Foo … end
consistently for the sake of reducing distractions.Struct
classes, or you use consistent syntax but then have to deal with the parent class mismatch problem if you ever want to redefine the class with a different set of members. In my case there were only two points in the book (pp. 32 & 36) where I needed to redefine such a class from scratch, so I paid the localised price of magically producingObject.remove_const
from nowhere rather than the global price of inconsistent syntax throughout the book. I’m sure there will be readers who bridle at this interruption, and perhaps I should’ve just picked a new class name rather than requiring these gymnastics, but again my gut feeling was that this weirdness was sufficiently orthogonal that the uninitiated reader could safely ignore it without feeling like they’d missed anything.Essentially, if I’d been writing a book about Ruby, I’d have gone for blocks. In this case I was trying to keep Ruby in the background as far as possible, so I went with classes, with the intention of avoiding the introduction of more ideas than absolutely necessary (at the potential cost of the reader’s actual understanding of Ruby!). That may yet turn out to be the wrong call, and I’d love to hear other people’s opinions.