❓
Immutable な Value Object とメモ化の相性が良くない件→解決
問題提起
例がいまいちだけど、次のように 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"
freeze 前にメモ化用のハッシュを用意しておけばよかった。解決!
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 を定義しておく。
同じ問題を取り扱った書籍
後日に読んだ『研鑽Rubyプログラミング ― 実践的なコードのための原則とトレードオフ』 の P.63 で「うまくいかないケース」としてこれと似た問題に言及されている。
Discussion
initialize で
@params = []
しておいて、params の中では@params[0] ||= ...
のようにすると、とりえあず動くようになりますね。コメントありがとうございます
self を freeze しても既存のインスタンス変数は変更できるんですね