この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
Rubyの大域脱出の紹介と、大域脱出のためにエラーを利用するなって話と、そもそも大域脱出を考える時は設計がおかしい可能性があるので考え直しましょうって話です。
はじめに
お疲れ様です、くろすです。
ようこそ、ここは黒魔術の入口だよ
大域脱出
rubyには大域脱出用のメソッドがあります。
throw..catchです。
普段書くようなコードで見るような関数ではないので、存在を知らない人も多いと思います。
動き的には普段見るコードで言う所のraise..rescue
みたいなヤツです。
深いところからズバッと抜けられるので気持ちがいいです。
ただraise..rescue
みたいなヤツと思って使うのはやめましょう、怪我の元です。
まず、raise..rescue
ですが、これは 例外処理 に使います。
例外クラスの部分を見ていただければわかると思いますが、ruby本体では例外をErrorの名を冠せず使ってる場所はかなり少ないです。
結果として大域脱出できますが、本質的にはエラーハンドリングのために存在しているよう思えます。
次に、throw..catch
ですが、これは大域脱出に使います。大域脱出専用です。
引数で指定したオブジェクトをthrowしないと怒られれます。
実用例
まず、エラーを大域脱出のために使っちゃったコードを見てみましょう。
なんとも運のいいことに僕
が過去記事で使っちゃってるんですね。
肥大化するAIと対峙する時に覚えておきたいこと
この記事こんなことが書いてあります。
(略)
class NotIntelligence < StandardError; end
def self.get_ai(code)
# ai = AIManager.get_ai(code).new(code)のように呼ばれた時の大域脱出で使う
raise NotIntelligence unless @codes_to_ai.keys.include?(code)
@codes_to_ai[code]
end
(略)
アホになれないcodeを割り振られた子たちはNotIntelligenceエラーを返されるという、なんとも分かりにくい説明コードを書いてしまいました。
この記事では肝心の大域脱出に当たる部分が書かれていませんが、おそらくこんな感じになるかと。
(というか AIManager.get_ai(code).new(code) ってめっちゃ気持ち悪いですね、このコード書いたヤツ誰
だ )
ai =
begin
AIManager.get_ai(code).new(code)
rescue
nil
end
さあ危険な匂いがして来ましたね!!!
これでも動きます。残念ながら動いてしまいます。
NotIntelligence は StandardErrorを継承しているのでrescue
で掬い上げられてしまいます。
全員が全部を理解して書いていれば問題ないのですが、そんなことは無理です。諦めましょう。
つまりこうすればいいんでしょ?
rescue NotIntelligence
と言われそうな気もしますが、そもそもNotIntelligence
はエラーではないのでrescue..raise
を使うのはやめましょう、というお話です。
throw..catch
で書き直すとこんな感じですね。
def self.get_ai(code)
unless @codes_to_ai.keys.include?(code)
throw :not_intelligence, nil
else
@codes_to_ai[code]
end
end
ai = catch :not_intelligence do
AIManager.get_ai(code).new(code)
end
そもそも使わなくていい
ここまで大域脱出について書きましたが、そもそも使わなくていい場合が多いです。設計を見直した方がいいと思います。
今回の場合はaiの定義場所を見たときどういう場合はaiが生成されないかを明確にしたかったのと、AIインスタンスを作成する場合にAIクラスが存在しない可能性があることを意識したくないので大域脱出を使用してますが、そもそもコメントで十分ですし、AIManager(Managerってクラス名も悪い)側で基本的なAIを必ず生成すれば使用する側からはAIがあるかないかを考えずに使用できます。
get_aiとかいうクラスを返す頭悪いコードもいらないですし、設計した私がアホです。
options = { code: code }
ai = AIBuilder.build_by(options)
class AIBuilder
def build_by(code: nil)
if @codes_to_ai.keys.include?(code)
@codes_to_ai[code].new(code)
else
DefaultAI.new
end
end
end
こんな感じの方がいいですね。AIBuilderには@codes_to_ai
が定義されてないので動かないんですけど。
じゃあどんな時に使えばいいのかという疑問がありますが、長いメソッドチェーンを途中で切り上げたい場合や、自前でvalidationを作る場合に他の処理と同列に書きたい場合などは大域脱出使えるんじゃないかと思います。
あまり見ないコードなのでわかりやすさは置いといてって感じになりますし、
効果的って意味じゃなく、あくまで使用可能って意味での「使える」って感じしかしないですけど。
複数ループの中から脱出したい & そのループの数をうまいこと減らそうとすると生成されてしまうarrayが要素数に比例してでかくなる & その要素数が外から操作できるので危険
みたいなすごく特殊な状況じゃないと使えない気がしますし、そんな状況だと普通の人ならメソッドにして値をreturnすると思います。
深い木構造の検索をかけて該当のものが見つかり次第脱出とかなら使えますかね……
もうちょっと面白い使い方ないかなぁ……
終わりに
関数途中のただ処理を切り上げるだけのreturn
って、個人的には広義のgotoな気がしてあんまり使いたくないから大域脱出の紹介したんですけど、throw..catch
ほど直接的にgotoな感じはしないからうーん…って感じです。return
に比べてthrow..catch
の方が意味を持って処理を切り上げられる上に深いところからも脱出できるんで使い勝手いいんじゃないかとは思いますが、そういう目的だと実際に使えるかは微妙ってところですかね。
大域脱出のことを考えてたら、考えが継続渡しに流れ、schemeならcall/ccだなーと考えつつググるとどうもrubyにもcallccが黒魔術としてあるらしいことを知り、でもそんなコード誰も読めなくなってしまうと闇に葬りました。
ただ闇のコードは書いてて楽しいので、そのうち何か記事にするかもしれません。