Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why shouldn't I extend an instance initialized by Struct.new?

Tags:

ruby

rubocop

We have a legacy codebase where rubocop reports some of these errors which I never could wrap my head around:

Don't extend an instance initialized by Struct.new. Extending it introduces a superfluous class level and may also introduce weird errors if the file is required multiple times.

What exactly is meant by "a superfluous class level" and what kind of "weird errors" may be introduced?

(Asking because obviously we didn't have any such problems over the last years.)

like image 562
awendt Avatar asked Mar 15 '18 08:03

awendt


2 Answers

Struct.new creates an anonymous class that happens to be a subclass of Struct:

s = Struct.new(:foo)
#=> #<Class:0x00007fdbc21a0270>

s.ancestors
#=> [#<Class:0x00007fdbc21a0270>, Struct, Enumerable, Object, Kernel, BasicObject]

You can assign that anonymous class to a constant in order to name it:

Foo = Struct.new(:foo)
#=> Foo

Foo.ancestors
#=> [Foo, Struct, Enumerable, Object, Kernel, BasicObject]

That's the regular way to create a Struct subclass.

Your legacy code on the other hand seems to contain something like this:

class Foo < Struct.new(:foo)
end

Struct.new creates an anonymous class (it's not assigned to a constant) and Foo subclasses it, which results in:

Foo.ancestors
#=> [Foo, #<Class:0x00007fee94191f38>, Struct, Enumerable, Object, Kernel, BasicObject]

Apparently, the anonymous class doesn't serve any purpose.

It's like:

class Bar
end

class Foo < Bar   # or Foo = Class.new(Bar)
end

Foo.ancestors
#=> [Foo, Bar, Object, Kernel, BasicObject]

as opposed to:

class Bar
end

class Foo < Class.new(Bar)
end

Foo.ancestors
#=> [Foo, #<Class:0x00007fdb870e7198>, Bar, Object, Kernel, BasicObject]

The anonymous class returned by Class.new(Bar) in the latter example is not assigned to a constant and therefore neither used nor needed.

like image 122
Stefan Avatar answered Oct 06 '22 00:10

Stefan


A superfluous class level is exactly this class entending Struct.new.

Here is the reference to a more detailed explanation with a source code.

The pull request on this cop also contains a valuable example:

Person = Struct.new(:first, :last) do
  SEPARATOR = ' '.freeze
  def name
    [first, last].join(SEPARATOR)
  end
end

is not equivalent to:

class Person < Struct.new(:first, :last)
  SEPARATOR = ' '.freeze
  def name
    [first, last].join(SEPARATOR)
  end
end

The former creates ::Person and ::SEPARATOR, while the latter creates ::Person and ::Person::SEPARATOR.

I believe the constant lookup is mostly referred as “weird errors.”

like image 28
Aleksei Matiushkin Avatar answered Oct 06 '22 00:10

Aleksei Matiushkin