Feature #11708
closedSpecify a way to override Struct-subclass constructor
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) about 9 years ago
- Assignee set to matz (Yukihiro Matsumoto)
Updated by duerst (Martin Dürst) about 9 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) about 9 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) about 9 years ago
- Status changed from Open to Closed
Updated by prijutme4ty (Ilya Vorontsov) almost 9 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 thatStruct.new do ... end
is preferred to subclassing. IfStruct.new
was defining aninitialize
(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.