RubyGemsには多様なGemがありますが、中にはRubyでも可能なことをC言語で実装して、高速化を図っているものがあります。今回は、そんなfast_blankを使っていたら引っかかったところがあった、そんな話です。
まず、blank?って?
こちらの記事を見てもらうとわかりやすいですが、blank?メソッドは、nil?だけではなく「空っぽい」オブジェクトに対してtrueを返す、ActiveSupport(Rails拡張)のメソッドです。具体的には、empty?があるものはその値につなぐ、文字列の場合は「ホワイトスペースだけ」ならtrue、というようになっています。
じつは、おそい
ActiveSupportのコードを見れば、String#blank?の実装はごくシンプルです。
class String
BLANK_RE = /\A[[:space:]]*\z/
# A string is blank if it's empty or contains whitespaces only:
#
# ''.blank? # => true
# ' '.blank? # => true
# "\t\n\r".blank? # => true
# ' blah '.blank? # => false
#
# Unicode whitespace is supported:
#
# "\u00a0".blank? # => true
#
# @return [true, false]
def blank?
BLANK_RE === self
end
end
Regexp#===が正規表現にヒットするかどうかの判定、ということさえわかれば、まさに「見ての通り」といった実装です。ただし、文字列に正規表現をヒットさせるという都合上、処理は意外と遅いです。
fast_blankの登場
そこで、このString#blank?を高速化するためのGemとして、fast_blankがあります。原理はシンプルで、「正規表現は使わずに、Cで繰り返し処理」というものでした。
こんな粒度でCエクステンションを入れて速くなるものなのかという疑問もありましたが、実際に速くなっているようです(GitHubのページにも測定結果があります)。
問題になる非互換
ただし、(Gem作者からすれば意図的ではあるのですが)fast_blankで入るString#blank?は、ActiveSupport版のものと違って、ASCIIの改行や空白しか考慮しないようになっています。つまり、全角スペースだけの文字列の場合、ActiveSupport版はblank?がtrueになりますが、fast_blank版ではfalseを返してしまいます。
この非互換が致命的に効いてくる場面は少ないかもしれませんが、日本語には全角スペースが現れてしまうので、放置するよりは対策したほうがいいでしょう。幸い、fast_blankでも空白の基準をActiveSupportと揃えたblank_as?メソッドを用意しているので、これに差し替えておきましょう。
if String.method_defined?(:blank_as?)
class String
alias_method :blank?, :blank_as?
end
end
もし何かの都合でfast_blankがなくてもエラーが出ないよう、外側にifを入れてあります。また、present?は、
class Object
# 中略
def present?
!blank?
end
# RDoc略
def presence
self if present?
end
end
というような実装なので、blank?さえ入れ替えればすべてが元に戻ります。