Live data from Hacker News

Abstract Methods and NotImplementedError in Ruby

nithinbekal.com

11–20 of 26 posts

Re: Abstract Methods and NotImplementedError in Ruby

#11
I have always been unconvinced of the value of doing Java-style abstract classes to assert a certain API in Ruby considering it's duck-typed.

Raising an error when at runtime you call a nonexistent method is native Ruby behavior, so why write methods that say "I don't exist"?

If doing this added in any build-time guarantees that you have to implement that method they'd be great, but as used they're just additional code for one to be aware of, slowing everyone down at code review and shifting the mental model away from the messages that are being passed.

If you need to document a duck type there probably are better ways to do it, and Ruby also provides inheritance hooks that could conceivably be used to detect the existence of methods at load time, but I've never seen that being used in the relatively numerous private code bases I've come across.

(Sorry for the numerous edits, the point became clearer after I posted this)

Re: Abstract Methods and NotImplementedError in Ruby

#12
post #10
post #7

Earlier quoted context omitted.

It's Ruby, there's no compilation. It's also common to not load all files of an application's codebase at startup, in particular this is the default setting for Rails in development mode.

There is. CRuby code gets compiled to ISeq (VM bytecode) upon `load` / `require` / `eval`. JRuby does similar things (IIRC using InvokeDynamic). The difference (and that does not even need to involve Rails) is that these three - and thus compilation - happen at runtime , anytime, always. The fact that a typical "main" file follows the "require first, then define modules and classes, then execute" creates a mental ill…

> thus compilation - happen at runtime, anytime, always.

To be a bit pedantic, you can compile Ruby code with `RubyVM::InstructionSequence.compile`, and then dump and load it as a String.

That's one of the things Bootsnap does to speedup boot time. And when you do that, there's no compilation at runtime.

But that doesn't change anything about OP's suggestion, it's still impossible to know if an interface will ever be implemented.

https://docs.ruby-lang.org/en/3.3/RubyVM/InstructionSequence...

Re: Abstract Methods and NotImplementedError in Ruby

#13
post #11

I have always been unconvinced of the value of doing Java-style abstract classes to assert a certain API in Ruby considering it's duck-typed. Raising an error when at runtime you call a nonexistent method is native Ruby behavior, so why write methods that say "I don't exist"? If doing this added in any build-time guarantees that you have to implement that method they'd be great, but as used they're just additional co…

> Raising an error when at runtime you call a nonexistent method is native Ruby behavior, so why write methods that say "I don't exist"?

Well, first for documentation as you pointed.

Second because otherwise the error is a `NoMethodError` which inherits from `StandardError` hence may be casually rescued.

> Ruby also provides inheritance hooks that could conceivably be used to detect the existence of methods at load time

It's not every practical because the hook is called while the child class is opened, so no method is yet implemented into it. You'd need to collect all the child class and then check they implement all the necessary methods at a later point when you are done loading, which isn't a clearly defined point in a general Ruby program.

Re: Abstract Methods and NotImplementedError in Ruby

#14
post #12
post #10

Earlier quoted context omitted.

There is. CRuby code gets compiled to ISeq (VM bytecode) upon `load` / `require` / `eval`. JRuby does similar things (IIRC using InvokeDynamic). The difference (and that does not even need to involve Rails) is that these three - and thus compilation - happen at runtime , anytime, always. The fact that a typical "main" file follows the "require first, then define modules and classes, then execute" creates a mental ill…

> thus compilation - happen at runtime, anytime, always. To be a bit pedantic, you can compile Ruby code with `RubyVM::InstructionSequence.compile`, and then dump and load it as a String. That's one of the things Bootsnap does to speedup boot time. And when you do that, there's no compilation at runtime. But that doesn't change anything about OP's suggestion, it's still impossible to know if an interface will ever be…

Indeed, I did not mention Bootsnap so as to detract the core point.

> That's one of the things Bootsnap does to speedup boot time. And when you do that, there's no compilation at runtime.

Correct, if one does `bootsnap precompile`.

Bootsnap hooks itself on `Kernel#load` + `Kernel#require` and `RubyVM::InstructionSequence#load_iseq`:

https://github.com/Shopify/bootsnap/blob/f627992c52642394311...

https://github.com/Shopify/bootsnap/blob/f627992c52642394311...

https://github.com/Shopify/bootsnap/blob/f627992c52642394311...

https://github.com/Shopify/bootsnap/blob/f627992c52642394311...

Essentially this means that it makes LOAD_PATH lookup faster + intercepts iseq compilation to store to cache on a miss and return a cached version on a hit.

That does not change the load order.

> But that doesn't change anything about OP's suggestion, it's still impossible to know if an interface will ever be implemented.

Again correct, as even with `bootsnap precompile` it would only result in earlier ISeq generation (it is essentially cache priming instead of doing it "lazily" on demand), not the time at which these ISeq get loaded and effective, so even then an `abstract` keyword would be ineffective (or rather, equivalent to raising NotImplementedError, i.e syntactic sugar)

The only way to do it outside of runtime is with type information, ensuring that only certain types respecting a contract reach the point where the expected interface is made use of. I'm using RBS+Steep, not just for that but this is one of the main reasons and I can attest it does work very well in practice: when the types don't respect the contract it blows up as expected, at which stage raising exceptions is either redundant or a defensive coding mechanism (and I've actually had Steep yell at me because some clauses were unreachable due to types ensuring the contract)

Re: Abstract Methods and NotImplementedError in Ruby

#15

How about... and please sit down for this one... How about adding an "abstract" keyword for methods and refusing to compile with an abstract method from a parent class not being implemented in a non abstract child class?

Ruby comes from the Alan Kay line of OO thinking, in which method dispatch is late bound via an object’s eigenclass. Aside from those burned into the standard library as native code, methods are defined at runtime. Bit of a chicken/egg problem for any compiler hoping to enforce dispatch invariants.

The C++/Java form of OOP is so conceptually divergent from Smalltalk-style OOP that it may be less confusing if we renamed it.

Re: Abstract Methods and NotImplementedError in Ruby

#16

How about... and please sit down for this one... How about adding an "abstract" keyword for methods and refusing to compile with an abstract method from a parent class not being implemented in a non abstract child class?

This isn’t how Ruby works. Classes can be modified at any point in the program’s runtime. Ie, methods might be added at instantiation or in response to some other event. And even for a boring static implementation, classes may be defined over multiple files. When exactly should you raise the error?

Re: Abstract Methods and NotImplementedError in Ruby

#17

I think that NotImplementedError doesn't get rescued is a feature, not a bug. It means you are calling a class that has a missing part of the interface, and that's usually programmer error. For some cases where you can partially implement the spec, it may make sense to go this way, but for most abstract classes I think the NotImplementedError is still a good idea.

that’s an accidental feature given that ruby’s own expectation, going by the description in the blog post, is that a required feature is missing on current platform. perhaps it’s badly named. because it leads to this faux ami. speaking as someone who has raised quite a lot of them myself, and i think that’s what sorbet raises as well.

Re: Abstract Methods and NotImplementedError in Ruby

#19
Back in the day I recall that NotImplementedError was used for metaprogramming dynamic methods - in particular the ActiveRecord helpers that translated to semi-natural language based on fields in the db... it would try to create the method on the fly if the fields existed and the convention was adhered to, otherwise it would rethrow. I don't believe those are supported anymore.

Re: Abstract Methods and NotImplementedError in Ruby

#20

`UndefinedMethodError` perhaps? that way we can restore the distinction between method declaration (which assigns a name, within some namespace) and method definition/implementation which gives the function its ability.

Wouldn't that be too similar to NoMethodError? I recently came across the idiom in Smalltalk, where you would call subclassResponsibility:

    someMethod:
      self subclassResponsibility
I've suggested the name in the ruby bug tracker issue here:

https://bugs.ruby-lang.org/issues/18915

Post reply on HN