Immutable な Value Object とメモ化の相性が良くない件

2022/11/03に公開
2

例がいまいちだけど次のようにURLを受け取ってURLの引数を簡単に取得できるクラスがあったとする

require "rack"

class FooUrl
  def initialize(url)
    @url = url
  end

  def xxx
    params["xxx"]
  end

  private

  def params
    @params ||= Rack::Utils.parse_query(URI(@url).query)
  end
end

foo_url = FooUrl.new("https://example.com/?xxx=1")
foo_url.xxx # => "1"

これはインスタンスに対して get しかしてないので Value Object と考えられる
Value Object だったら Immutable にした方がいいだろう
というわけでコンストラクタの最後に freeze を入れてみる

require "rack"

class FooUrl
  def initialize(url)
    @url = url
    freeze
  end

  def xxx
    params["xxx"]
  end

  private

  def params
    @params ||= Rack::Utils.parse_query(URI(@url).query)
  end
end

foo_url = FooUrl.new("https://example.com/?xxx=1")
foo_url.xxx # =>

するとこれ

# ~> -:16:in `params': can't modify frozen FooUrl (FrozenError)
# ~>    from -:10:in `xxx'
# ~>    from -:21:in `<main>'

freeze する前に params を実行しておくとかダサすぎるしいったいどうしたらいいんだともう何年も悩んでいる

というのを投稿したらコメントで良いアイデアを教えていただいたのでこうしてみた

require "rack"

class FooUrl
  def initialize(url)
    @url = url
    @cache = {}
    freeze
  end

  def xxx
    params["xxx"]
  end

  private

  def params
    @cache[:params] ||= Rack::Utils.parse_query(URI(@url).query)
  end
end

foo_url = FooUrl.new("https://example.com/?xxx=1")
foo_url.xxx # => "1"

これでええやん
解決!

Data クラスを使うときも同様の方法で行ける

Vector = Data.define(:x, :y) do
  def initialize(...)
    @cache = {}
    super
  end

  def length
    @cache[:length] ||= Math.sqrt(x**2 + y**2)
  end
end

vector = Vector.new(1, 2)
vector.length                  # => 2.23606797749979

親クラスの initialize で freeze されているっぽいので super を呼ぶ前に @cache を定義しておく

Discussion

Kenta MurataKenta Murata

initialize で @params = [] しておいて、params の中では @params[0] ||= ... のようにすると、とりえあず動くようになりますね。

megetonmegeton

コメントありがとうございます
self を freeze しても既存のインスタンス変数は変更できるんですね

ログインするとコメントできます