Project

General

Profile

Actions

Feature #11708

closed

Specify a way to override Struct-subclass constructor

Added by prijutme4ty (Ilya Vorontsov) about 6 years ago. Updated almost 6 years ago.

Status:
Closed
Priority:
Normal
Target version:
-
[ruby-core:71553]

Description

It's common to create simple data-object with some constraints. One can either implement custom class or use Struct. Struct is generally simpler and helps to avoid some mistakes as non-defined #hash and #eql?. But at the same time it's more difficult to make validation for Struct subclass.

Point = Struct.new(:x, :y)

NonnegativePoint = Struct.new(:x,:y) do
  def initialize(*args, &block)
    super
    raise 'Negative coordinates are not allowed'  if x < 0 || y < 0
  end
end

Above written code solves the problem but has one flaw. Struct.new creates a subclass of Struct and defines some methods as #x, #x=. And there are no guarantees that NonnegativePoint#initialize wasn't redefined too.
We can check that Point.new without explicitly defined #initialize actually hits Struct#initialize and Point#initialize not defined:

Point.instance_method(:initialize)
# => #<UnboundMethod: Point(Struct)#initialize>
NonnegativePoint.instance_method(:initialize)
# => #<UnboundMethod: NonnegativePoint#initialize>

But nothing in Struct documentation or test suite states that this behavior can't be changed in newer ruby versions.

I propose either to declare in docs and test that initialize method can be safely overriden because #initialize is not defined in Struct subclasses.
In you assume that one day current behavior can change (e.g. for perfomance reasons), then it's reasonable to create an extension point like '#after_initialize' which is called from Struct's subclass #initialize method.

Updated by prijutme4ty (Ilya Vorontsov) almost 6 years ago

  • Assignee set to matz (Yukihiro Matsumoto)

Updated by duerst (Martin Dürst) almost 6 years ago

I'm not Matz, but in general, everything in Ruby is dynamic. The documentation doesn't say for each feature that it is going to be kept dynamic in the future. That would be a lot of useless text in the documentation.

Updated by marcandre (Marc-Andre Lafortune) almost 6 years ago

It's true that there is no test (and more surprisingly no Rubyspec) on this.

I'm not Matz either, but I feel there's no way that this behavior will ever change. First because there's no reason why it would, but more importantly because it would be a source of incompatibility and frustration.

I feel you can deduce that initialize is overridable from the current doc, as it states that Struct.new do ... end is preferred to subclassing. If Struct.new was defining an initialize (and thus preventing easy overriding of initialize), then there would be a caveat since subclassing would not have this problem.

I added a test and hope this will resolve conclusively this issue.

Updated by marcandre (Marc-Andre Lafortune) almost 6 years ago

  • Status changed from Open to Closed

Updated by prijutme4ty (Ilya Vorontsov) almost 6 years ago

  • Assignee changed from matz (Yukihiro Matsumoto) to marcandre (Marc-Andre Lafortune)

Marc-Andre Lafortune wrote:

It's true that there is no test (and more surprisingly no Rubyspec) on this.

I'm not Matz either, but I feel there's no way that this behavior will ever change. First because there's no reason why it would, but more importantly because it would be a source of incompatibility and frustration.

I feel you can deduce that initialize is overridable from the current doc, as it states that Struct.new do ... end is preferred to subclassing. If Struct.new was defining an initialize (and thus preventing easy overriding of initialize), then there would be a caveat since subclassing would not have this problem.

I added a test and hope this will resolve conclusively this issue.

First, thank you for the test.
And second, no. It can't be deduced. Actually, I was rather surprised that my approach works. Look,

Point=Struct.new(:x,:y) do
  def x=(val)
    puts "x=#{val}"
    super
  end
end

pt = Point.new(1,2)
pt.x = 3 # => NoMethodError: super: no superclass method `x=' for #<struct Point x=1, y=2>

There were no guarantees that initialize redefinition works different from accessors redefinition. After reading documentation I have no clue, what actually happens when I call Struct.new. Ok, it creates a class, which is a subclass of Struct. But it's hard to say whether Struct#initialize exists at all (because .new obviously doesn't follow allocate-initialize pattern, it doesn't even return an instance of Struct class). And if it exists, which arguments does it accept? does it store all provided arguments and so on?
For perfomance reasons it looks much more natural that Struct.new creates a class and dynamically defines both accessors and a custom #initialize method with specific number of arguments. Moreover it looks natural to define a constructor with kwargs (which looks like a quite probable direction for future improvements of Struct usability). And, again, it might be simpler to do it with dynamic constructor definition than using superclass constructor.
And finally, it is not obvious that methods written in Struct.new's block go right into created class itself. It might be more reasonable to define them in a separate anonymous module and then to prepend that module into a class. It will resolve problem with redefining accessors as in my example.

I'll try to formulate these suggestions in separate issues. These examples were just to show, why I don't think this behavior is easily understandable and why I think documentation is rather unclear.

Actions

Also available in: Atom PDF