How to work through name collisions in ruby
Asked Answered
H

4

7

Two modules Foo and Baa respectively define a method with the same name name, and I did include Foo and include Baa in a particular context.

When I call name, how can I disambiguate whether to call the name method of Foo or Baa?

Halliehallman answered 24/9, 2018 at 18:22 Comment(0)
A
1

Only the order of modules inclusion decides which one will get called. Can't have both with the same name - the latter will override the former.

Of course, you can do any tricks, just from the top of my head:

module A
  def foo
    :foo_from_A
  end
end

module B
  def foo
    :foo_from_B
  end
end

class C
  def initialize(from)
    @from = from
  end

  def foo
    from.instance_method(__method__).bind(self).call
  end

  private

  attr_reader :from
end

C.new(A).foo #=> :a_from_A
C.new(B).foo #=> :a_from_B

But that's no good for real life use cases :)

Aldarcie answered 24/9, 2018 at 18:26 Comment(0)
C
0

Technically, there is no name collision because the method foo is redefined.

In the following exemple, A.foo is redefined and is never called

module A
  def foo
    raise "I'm never called"
  end
end

module B
  def foo
    puts :foo_from_B
  end
end

class C
  include A
  include B
end

C.new.foo
# =>
# foo_from_B

If you write A and B module, you can use super to call previous definition of foo. As if it where an inherited method.

module A
  def foo
    puts :foo_from_A
  end
end

module B
  def foo
    super
    puts :foo_from_B
  end
end

class C
  include A
  include B
end

C.new.foo
# =>
# foo_from_A
# foo_from_B

There are side effects and I would not use this but this is doing the trick :

module A
  def foo
    puts :foo_from_A
  end
end

module B
  def foo
    puts :foo_from_B
  end
end


class C
  def self.include_with_suffix(m, suffix)
    m.instance_methods.each do |method_name|
      define_method("#{method_name}#{suffix}", m.instance_method(method_name))
    end
  end
  include_with_suffix A, "_from_A"
  include_with_suffix B, "_from_B"
end

c= C.new
c.foo_from_A
c.foo_from_B
begin
  c.foo
rescue NoMethodError
  puts "foo is not defined"
end
# =>
# foo_from_A
# foo_from_B
# foo is not defined
Ceramic answered 24/9, 2018 at 18:42 Comment(0)
E
0

Provided none of the methods of Foo or Baa call name (which seems a reasonable assumption), one can simply create aliases.

module Foo
  def name; "Foo#name"; end
end

module Baa
  def name; "Baa#name"; end
end

class C
  include Foo
  alias :foo_name :name
  include Baa
  alias :baa_name :name
  undef_method :name
end

c = C.new
c.foo_name
  #=> "Foo#name"
c.baa_name
  #=> "Baa#name"

C.instance_methods & [:foo_name, :baa_name, :name]
  #=> [:foo_name, :baa_name]

The keyword alias is documented here. One may alternatively use the method #alias_method. See this blog for a comparison of the two.

Module#undef_method is not strictly necessary. It's just to ensure that an exception is raised if name is called.

Emaciation answered 24/9, 2018 at 20:18 Comment(8)
wow, nice! where can I find the official documentation for aliasHalliehallman
I see all the answers have collected downvotes, suggesting there may be a rogue downvoter in our midst. Asking downvoters to come out of the shadows and explain themselves is futile. The only explanations you will see are those given without your prompting, from members who believe they have a duty to explain why they downvoted.Emaciation
I did downvote this. This code looks like hack: it utilizes include order and nature of alias (btw, you don't have to use symbols in alias). Though every ruby developer should understand how this code works it seem to complicated and smelly. My suggestion utilizes UnboundMethod with no magic at all. Relating on included modules order doesnt seem like a smart thing to do.Aristotle
@Nondv, I don't understand your point about the order in which modules are included, as the ordering is arbitrary. Also, I don't see why the method I proposed is complicated or employs "magic", as aliasing is a simple concept that is routinely used to avoid naming collisions. I do agree that my code smells, much like freshly-baked bread.Emaciation
if we discriminate against "include order", then we're going against the lisp style, which I feel is the shoulder that ruby stands on.Halliehallman
@CarySwoveland I don't agree that aliasing is a simple concept. It basically creates a new method, not "alias". Every ruby developer should know this but it doesnt make it less magical. I, for one, despise the fact that it is called "aliasing".Aristotle
@CarySwoveland as for the include order: maybe it's just me, but I don't think that one should rely on order at all. One should keep it simple: module just provides methods, you don't need to dive into details with method lookup and whatsoever. My point here is that I expect any class to include modules at start and not to use something in between. Anyway, I think that the issue signals that something wrong with design in general: I can't even imagine case, when you include two modules with similar methods.Aristotle
@Halliehallman can you elaborate on lisp style?Aristotle
A
0

You should definetely read about method lookups.

Anyway, I would do it this way:

module Foo
  def name
    :foo
  end
end

module Bar
  def name
    :bar
  end
end

class MyClass
  include Foo
  include Bar

  def foo_name
    Foo.instance_method(:name).bind(self).call
  end

  def bar_name
    Bar.instance_method(:name).bind(self).call
  end

  #
  # or even like this: obj.name(Foo)
  #

  def name(mod)
    mod.instance_method(:name).bind(self).call
  end
end

BTW if you are using Module#instance_method and UnboundMethod#bind you don't really need to include specific module. This code works:

Foo.instance_method(:name).bind('any object (e.g. string)').call
Aristotle answered 25/9, 2018 at 10:3 Comment(2)
While not my preference, this is an interesting approach, worthy of an upvote. Regarding your last comment, you should make it clear that Foo need not be included only if it contains no methods that are called by name. Also, some readers may think you mean that the method could be expected to work if it is bound to any object, which, of course, is generally not the case, as included methods commonly expect the receiver to be an instance of a particular class.Emaciation
@CarySwoveland it's not my preference either ;) The whole situation is stupid. One should refactor system, not look for hacks to resolve name collisions.Aristotle

© 2022 - 2024 — McMap. All rights reserved.