2022/08/18 今回の気になった bugs.ruby のチケット

今週はメモ化を行う APIRuby のコア機能に入れる議論がありました。

[eature #18934] Proposal: Introduce method results memoization API in the core

  • メモ化する APIRuby のコア機能に導入しようという提案
    • メモ化とは1回目のメソッドの結果を保存しておき、2回目以降はその結果を使い回す機能の事
class Test
  def foo
    puts "call!"
    5
  end

  # foo メソッドをメモ化する
  memoized :foo
end

o = Test.new

# 1回目の呼び出しはメソッド本体が呼び出される
o.foo # prints "call!", returns 5

# 2回目以降はメソッドの本体は呼び出されず1回目の結果を返す
o.foo # returns 5 immediately
  • 具体的な例として以下のようなコードが上げられている
    • SomeTokenizer#call が非効率なメソッドの場合にボトルネックになる可能性がある
class Sentence
  attr_reader :text

  def initialize(text) = @text = text

  def tokens() = SomeTokenizer.new(text).call

  def size() = tokens.size
  def empty?() = tokens.all?(&:whitespace?)

  def words() = tokens.select(&:word?)
  def service?() = words.empty?
end

many_sentences
  .reject(&:empty?)
  .select { _1.words.include?('Ruby') }
  .map { _1.words.count / _1.tokens.count }
  • メモ化の手段として ||= を使うイディオムもある
def words()= @words ||= tokens.select(&:word?)
  • しかし、イディオムにはいくつか欠点があり例えば service? のように nil / false を返すようなメソッドの場合にはその都度処理が呼ばれてしまう
  • なのでそれを回避する場合は以下のように定義する必要がある
def empty?
  # @empty が定義されている場合のみ tokens.all?(&:whitespace?) を呼び出す
  return @empty if defined?(@empty)

  @empty = tokens.all?(&:whitespace?)
end
  • またインスタンス変数を使うことで #inspect の結果などに保持している値が含まれてしまう
class Sentence
  def initialize(text)
    @text = text
  end

  def empty?
    # @empty が定義されている場合のみ tokens.all?(&:whitespace?) を呼び出す
    return @empty if defined?(@empty)

    @empty = @text.empty?
  end
end

require "yaml"

s = Sentence.new('Ruby is cool')
puts s.to_yaml
# => --- !ruby/object:Sentence
#    text: Ruby is cool

p s.empty?
# => false

# s の中身が汚染されてしまう
puts s.to_yaml
# --- !ruby/object:Sentence
# text: Ruby is cool
# empty: false
  • メモ化するライブラリとして memoistmemo_wise が紹介されている
    • memo_wise の方が新しくて効率がいいらしい
    • これらのライブラリでは nil / false の戻り値にも対応している
# 使用例
class Sentence
  prepend MemoWise
  # ...
  memo_wise def empty?() = tokens.all?(:whitespace?)
end
  • ただし、上記のライブラリを使った場合でもインスタンス変数を使っているのでオブジェクトが汚染されてしまう
p s.empty?
# false
p s
# #<Sentence:0x00007f0f474eb418 @_memo_wise={:empty?=>false}, @text="Ruby is cool">
puts s.to_yaml
# --- !ruby/object:Sentence
# _memo_wise:
#   :empty?: false
# text: Ruby is cool
  • なのでこれを解決するためにインスタンス変数を使わないで C で実装された Module#memoized(*symbols) をコア機能として提供する、というのがこのチケットでやりたいことになる
  • コアにあると便利ではあると思うが C API を使った gem とかつくれないのかな
  • あとはメモ化はクラス全体が immutable でないと安全ではないので意図的にインスタンス変数などを変えた場合に誤った動作をする、みたいな事がコメントで指摘されていますね