Sorbet: Modules don't recognize methods inherited from Object

Created on 27 Nov 2019  Â·  3Comments  Â·  Source: sorbet/sorbet

Input

→ View on sorbet.run

# typed: true

class Object
  def foo; end
end

module MyModule
  def bar
    foo
  end
end

Observed output

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

Expected behavior

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.

→ View on sorbet.run

# 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
bug unconfirmed

Most helpful comment

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.

All 3 comments

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!

→ View on sorbet.run

# 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.

→ View on sorbet.run

# 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.

Was this page helpful?
0 / 5 - 0 ratings