# typed: true
class Object
def foo; end
end
module MyModule
def bar
foo
end
end
editor.rb:9: Method foo does not exist on MyModule https://srb.help/7003
9 | foo
^^^
editor.rb:4: Did you mean: Object#foo?
4 | def foo; end
^^^^^^^
Errors: 1
This is completely valid Ruby code. When calling foo inside MyModule, Ruby will run Object#foo.
Same if foo is defined outside a class, which is the same as defining it inside Object.
# typed: true
def foo; end
module MyModule
def bar
foo
end
end
editor.rb:7: Method foo does not exist on MyModule https://srb.help/7003
7 | foo
^^^
editor.rb:3: Did you mean: Object#foo?
3 |def foo; end
^^^^^^^
Errors: 1
Sorbet's behavior here is intentional, because there's no guarantee from Ruby that MyModule is going to always be included in something that inherits from Object. For example, consider the following code:
class Object
def foo; end
end
module MyModule
def bar; foo; end
end
class MyClass < BasicObject
include ::MyModule
end
If I run MyClass.new.bar, then Ruby will give me the error undefined local variable or method "foo" for #<MyClass:...>. Sorbet prevents this statically by not assuming that all methods on Object are present in a module context.
Thanks @aisamanra!
I tried to find some solutions/workarounds for this (see below). You might give me a thought on them.
But I think the best solution would be a way to specify, that MyModule can only be used in classes inheriting from Object. Something like:
module MyModule
T::Helpers
must_be_a(Object)
end
Did I overlook, that something like this (or a even better solution) exists?
An Object Interface
This gets pretty close to the idea of must_be_a(...).
It would be great if Sorbet would provide such interfaces for all core and stdlib classes/modules, especially Object!
# typed: true
module CoreInterfaces::Object
extend T::Sig
extend T::Helpers
interface!
sig {abstract.params(m: Module).returns(T::Boolean)}
def is_a?(m); end
end
module MyModule
extend T::Helpers
include CoreInterfaces::Object
abstract!
def bar
is_a?(String)
end
end
Using Abstract
I found this workaround/solution rather astonishing, because I didn't expected it to work.
→ View on sorbet.run
# typed: true
class Object
def foo
puts('foo')
end
end
module MyModule
extend T::Sig
extend T::Helpers
abstract!
# abstract prevents MyModule#foo from overwriting Object#foo
sig {abstract.void}
def foo; end
def bar
foo
end
end
class Test
include MyModule
end
Test.new.bar
# => foo
At first I thought this wouldn't work, because MyModule#foo would overwrite Object#foo. And without abstracts thats exactly what happends. But it looks like theabstractproperty prevents this behavior, soObject#foo` isn't being overwritten.
So what about creating a hook for this kind of situation: Did you mean: Object#foo?
So the suggestion would instead be:
Consider declaring foo as abstract.
sig {abstract[...]}
def foo; end
Putting Methods into Kernel instead of Object
A rather ugly workaround I think, because you need to change the Ruby code you wanted to write. Also this won't work for globally defined methods.
# typed: true
module Kernel
def foo; end
end
module MyModule
include Kernel
def bar
foo
end
end
I really like the abstract solution because I think it expresses what's meant here — the includer must implement this method (possibly by also including Kernel).
Maybe what's missing is a helper to define all of the abstract methods from another module? Kind of like the must_be_a suggestion, but with structural rather than nominative types (which avoid introducing any new concepts into Sorbet's representation of modules).
Eg.
module MyModule
T::Helpers
includer_must_implement! Object
includer_must_implement! MyClass
end
Which would look at the interfaces of Object and MyClass, copy all of the method definitions and define them as abstract on MyModule. Then module authors wouldn't need to re-define all of the Object/Kernel etc. methods that they want to use.
Most helpful comment
Sorbet's behavior here is intentional, because there's no guarantee from Ruby that
MyModuleis going to always beincluded in something that inherits fromObject. For example, consider the following code:If I run
MyClass.new.bar, then Ruby will give me the errorundefined local variable or method "foo" for #<MyClass:...>. Sorbet prevents this statically by not assuming that all methods onObjectare present in a module context.