ruby-regexp_trie gemのソースコードを見ると予想以上に短く、1画面に収まるほどでした。
class RegexpTrie
# @param [Array<String>] strings Set of patterns
# @param [Fixnum,Boolean] option The second argument of Regexp.new()
# @return [Regexp]
def self.union(*strings, option: nil)
new(*strings).to_regexp(option)
end
def initialize(*strings)
@head = {}
strings.flatten.each do |str|
add(str)
end
end
これなら読めそうと思ったのですが、冒頭のクラスメソッドでnew(*strings)
と書いているのを見てしばらく考えてしまいました😅。私は業務でRubyのコードを書いていないこともあって、この書き方は初めてでした。
おそらくRegexpTrie.union
でもRegexpTrie.new
でもやれるようにするための書き方なのだろうと推測しました。以前書いた記事のmodule_function
をちょっと思い出しましたが、RegexpTrie
はモジュールではなくクラスです。
はみだしつっつき
いつもお世話になっているkazzさんに質問しました。
「ああ、クラスメソッドでそうやってnew()
だけ書くのはときどきやりますよ🧐」「そうでしたか!ありがとうございます」「newの後処理とかもインスタンス生成側に要求したいときは、自分もしょっちゅうこれで書きますし😋」「クラスメソッドの引数をそのまま引数に渡しているんですね: 最初見たときは、『クラスメソッドなのにnew
するの?』と思っちゃいました😅」
※クラスメソッドでnew
するのは「典型的なfactory method」だとmorimorihogeさん&babaさんに教わりました🙇。
参考: Factory Method パターン - Wikipedia
処理を2つに分ける
「ruby-regexp_trieでプリントデバッグしながら処理を追ってみたんですけど、どうやら処理をadd
とbuild
の2つに分けてやってることがわかりました」
# @param [String] str
def add(str)
return self if !str || str.empty?
entry = @head
str.each_char do |c|
entry[c] ||= {}
entry = entry[c]
end
entry[:end] = nil
self
end
処理後の
@head
:{"課"=>{"長"=>{:end=>nil, "補"=>{"佐"=>{}}}}}
「add
は入力の配列を1文字ずつハッシュのキーにして上のようなツリーを組み立てて@head
に保存しておいて、それが終わってからbuild
でそのツリーを正規表現の形にビルドしていました」「そうそう、複雑な処理を2つに分けるというのはとってもいいこと❤️: 1つのループの中で何もかもやろうとするとどんどんつらくなりますし」「ですね!」
「特に上のadd
はeach_char
の中に1つも分岐を書いてないのがいい👍: ループの中に分岐があればあるほどテストがつらくなるんですよ😢」「そうでしたか!」
def build(entry)
return nil if entry.include?(:end) && entry.size == 1
alt = []
cc = []
q = false
entry.keys.each do |c|
if entry[c]
recurse = build(entry[c])
qc = Regexp.quote(c)
if recurse
alt.push(qc + recurse)
else
cc.push(qc)
end
else
q = true
end
end
cconly = alt.empty?
unless cc.empty?
alt.push(cc.size == 1 ? cc.first : "[#{cc.join('')}]")
end
result = alt.size == 1 ? alt.first : "(?:#{alt.join('|')})"
if q
if cconly
"#{result}?"
else
"(?:#{result})?"
end
else
result
end
end
「トライ木のロジックはbuild
の方にまとまってますね」「なお、ツリーの仕様が変わらないのであればこういう書き方がいいんですけど、CSVのパースみたいに後から仕様変更されがちなコードでは、こうやってキレイに2つに分けられるとは限らないですね(↓関連記事見てね❤️)」「そうか、正規表現なら普通書き方が変わらないからこれがいいんですね」