Ruby用于Mixins的线性化算法是什么?


Ruby中,模块可以包含其他模块,作为多重继承的形式.为了测试这一点,我根据一篇关于C3线性化的文章中的一个例子编写了以下程序:

module O
  def doIt()
    super if defined?(super)
    puts "O"
  end
end

module F
  include O
  def doIt()
    super if defined?(super)
    puts "F"
  end
end

module E
  include O
  def doIt()
    super if defined?(super)
    puts "E"
  end
end

module D
  include O
  def doIt()
    super if defined?(super)
    puts "D"
  end
end

module C
  include F
  include D
  def doIt()
    super if defined?(super)
    puts "C"
  end
end

module B
  include E
  include D
  def doIt()
    super if defined?(super)
    puts "B"
  end
end

class A
  include C
  include B
  def doIt()
    super if defined?(super)
    puts "A"
  end
end

A.new.doIt

哪个(在Ruby 1.9.3上)输出:

O
F
E
D
C
B
A

在这种情况下,Ruby如何确定方法解析顺序?

最佳答案 我在这里写了一篇描述算法的文章:
http://norswap.com/ruby-module-linearization/

粘贴在最重要的位:

Include = Struct.new :mod
Prepend = Struct.new :mod

# Returns the ancestry chain of `mod`, given the environment `env`.
#
# No distinctions are made between classes and modules: where a class extends
# another class, that class is treated as the first included module.
#
# params:
# - mod: Symbol
#   Represents a module.
# - env: Map<Symbol, Array<Include|Prepend>>
#   Maps modules to inclusions, in order of apperance.

def ancestors (mod, env)
  chain = [mod]
  includes = env[mod]
  includes.each { |it| insert(mod, it, env, chain) }
  chain
end


def insert (mod, inclusion, env, chain)
  i = inclusion.is_a?(Prepend) ? 0 : 1
  ancestors(inclusion.mod, env).each do |it|
    raise ArgumentError('cyclic include detected') if it == mod
    j = chain.find_index it
    if not j.nil?
      i = j + 1 if j >= i
      next
    end
    chain.insert(i, it)
    i += 1
  end
end

Let’s try to get some intuition in there. Let’s start with some
observations:

  • If B is included after A, B take precedence over A.

  • If B extends or includes A, B takes precedences over A.

  • When including a module X, the algorithm tries to maintain the ordering of the ancestor chain of X.

  • If there is a conflict of ordering, the algorithm always favors the ordering of modules that were included earlier.

The last point highlight the most unintuitive thing about the
algorithm: modules included later take precedence, but when
considering conflicts of ordering, it’s modules included earlier that
win!

The reason is that in Ruby, we may include a module in another at any
time. Ruby never reorders modules in the ancestor chain, but has no
problem inserting modules in between existing modules. This does make
sense, but leads to puzzling linearization behaviour.

Making module included earlier take precedence would solve the
problem. However when we include a module in another at runtime, we
usually would like it to take precedence on previously included
modules!

点赞