MemoWise中的Esoteric Ruby

103 阅读3分钟

我们最近发布了一个记忆化的精品--MemoWise!我们已经写了关于它的起源故事性能。在这篇文章中,我们将讨论我们在编写MemoWise时遇到的Ruby的一些深奥的角落。

用prepend对冻结对象进行记忆

在创建这个 gem 时,我们需要支持的功能之一是对冻结或不可变的对象进行备忘。具体来说,我们使用Values gem来创建不可变的实例。一旦一个对象被冻结,我们就不能分配它的任何实例变量:

class Example
 attr_writer :value
end
Example.new.freeze.value = true # FrozenError (can't modify frozen Example)

这与记忆化有什么关系?大多数记忆化工具的工作原理是创建一个哈希值来存储记忆化的值。这个哈希值通常是对象本身的一个实例变量。(我们称我们的 @_memo_wise.)因此,如果对象是不可变的,这个实例变量对象被冻结不能被分配。(然而,它可以被变异)。

这就是为什么我们prepend MemoWise模块来实现记忆化。 prepend的知名度低于 includeextend的知名度较低,其工作方式也与这两者略有不同。

每个Ruby类都有一个祖先的列表。这个列表包含了所有包含或预设的模块,按继承顺序排列。我们可以看一下Array ,作为一个例子:

Array.ancestors
=> [Array, Enumerable, Object, Kernel, BasicObject]

当一个方法在一个对象上被调用时,Ruby会依次查看每个祖先,看这个方法是否在这个对象的任何一个祖先上被定义。当我们include 一个模块时,该模块被插入到类的后面:

module Example; end

class Array
  include Example
end

Array.ancestors
=> [Array, Example, Enumerable, Object, Kernel, BasicObject]

当我们extend 一个模块时,该模块的方法被导入为类上的方法,并且该模块不会被插入到祖先链中:

class Array
  extend Example
end

Array.ancestors
=> [Array, Enumerable, Object, Kernel, BasicObject]

当我们prepend 一个模块时,该模块被插入到类之前 prepending 它的祖先链中。模块的方法将优先于类的方法:

class Array
  prepend Example
end

Array.ancestors
=> [Example, Array, Enumerable, Object, Kernel, BasicObject]

这反过来允许我们覆盖initialize 方法,并在对象被冻结之前创建记忆化哈希。这就是MemoWise如何允许冻结对象的记忆化。

确定方法的可见性

MemoWise的另一个重要功能是保留方法的可见性。如果有人使用我们的gem将一个私有方法备忘化,我们要保证备忘化的方法仍然是私有的。

实际上,并没有一个内置的Ruby方法来获取一个方法的可见性。然而,我们可以通过结合各种内置方法来确定可见性:

if private_method_defined?(method_name)
  :private
elsif protected_method_defined?(method_name)
  :protected
elsif public_method_defined?(method_name)
  :public
else
  raise NoMethodError
end

使用这个:private,:protected:public 符号,我们就可以动态地设置我们的新方法的可见性,以匹配原来的方法。

支持用allocate以及new创建的对象

在测试这个gem的早期版本时,我们遇到了对ActiveRecord 类的记忆错误。这些错误表明我们的@_memo_wise 实例变量没有被设置,这让我们很惊讶,因为正如前面提到的,我们在initialize 中设置了它。

经过大量的调试,我们了解到在Ruby中有一个鲜为人知的方法,可以在不执行initialize 方法的情况初始化一个对象。它被称为allocate ,它被Rails的ActiveRecord 使用。

当我们在一个类上调用new ,在引擎盖下发生的事情看起来是这样的:

class Example
  def self.new(...)
    allocate.tap { |instance| instance.initialize(...) }
  end
end

通过使用allocate 而不是new ,Rails绕过了我们对@_memo_wise 的初始化。为了解决这个问题,我们必须重写allocate 来执行这个初始化

展望未来

这些是我们在开发MemoWise时学到的关于Ruby的一些有趣的东西,我们计划在将来再写一些其他的东西同时,请尝试一下或在GitHub上阅读代码,我们很乐意接受贡献。