2020年12月23日
プログラミング
Ruby 後置whileの挙動をみる
新しい技術にチャレンジし続けるpalanのアドベントカレンダーDay23です。
昨日は「AR.jsでLocation Based ARを作ってみる」という記事でした。
はじめに
先週書いたEnumerableモジュールの繰り返し処理に続き、本日は後置whileを使った繰り返し処理について書いていきます。
まず、テーマ選定の背景を簡単に書いていきます。
以前、オブジェクトにUUIDを割り当てるために下記①の処理を作成したのですが、Rubocopの警告を受け②に修正をしました。
# ①後置whileを使用
def set_uuid
begin
tmp_uuid = SecureRandom.alphanumeric(12)
end while Picture.find_by(uuid: tmp_uuid)
self.uuid = tmp_uuid
end
# ②loopを使用
def set_uuid
self.uuid = loop do
tmp_uuid = SecureRandom.alphanumeric(12)
break tmp_uuid unless Picture.find_by(uuid: tmp_uuid)
end
end
私個人としては後置whileの方が自然に上から処理の流れを追うことができるため読みやすいなと考えて書いたのですが、Rubyスタイルガイドに従い修正しました。
ところが最近whileが速いという話を聞き、なぜ処理速度が速い後置whileを使ってはいけないのか調べてみようと記事にすることにしました。
それでは本題へ移っていきます。
なお、本記事における実行環境は、2020年12月23日時点での最新の安定版である2.7.2を使用しています。
環境
- Ruby: 2.7.2
後置whileはloopより速い
速度検証
まず、本当にwhileが速いのかを検証するために、後置whileとloopのベンチマークを取ってみました。
検証コードはRuby 2.7.0 リファレンスマニュアルを参考にして作成しました。
処理の内容としては空の配列を作成して 1 から 50,000 までの処理を順番に格納するものとなっています。
require 'benchmark'
n = 50_000
Benchmark.bm do |x|
x.report("loop:") do
ary = []
count = 0
loop do
ary << count
count += 1
break if count >= n
end
end
x.report("end_while:") do
ary = []
count = 0
begin
ary << count
count += 1
end while count <= n
end
end
結果
Rubyの処理が動いたCPU 時間で比較するために、ユーザCPUを見ていきます。
また、結果を見やすくするためにloop処理 -> loop: 、 後置while -> end_while と識別子をつけて実行しました。
1回目
下記は1回目の実行結果です。loopが0.003378秒かかったのに対し、end_whileは0.001390秒で処理が完了している。後置whileの方が2倍以上速く処理が実行されていることが分かります。
user system total real
loop: 0.003378 0.000193 0.003571 ( 0.003640)
end_while: 0.001390 0.000152 0.001542 ( 0.001594)
2回目
user system total real
loop: 0.003064 0.000128 0.003192 ( 0.003259)
end_while: 0.001416 0.000187 0.001603 ( 0.001733)
3回目
user system total real
loop: 0.003095 0.000146 0.003241 ( 0.003278)
end_while: 0.001361 0.000166 0.001527 ( 0.001592)
2回目、3回目と実行してみましたが、結果に大きな差異はなく後置whileの方が速いという結果となりました。
後置whileはなぜ非推奨なのか
RuboCopの公式ドキュメントを参照したところ、下記のように書かれていました。
This cop checks for uses of begin…end while/until something.
The cop is marked as unsafe because behaviour can change in some cases, including if a local variable inside the loop body is accessed outside of it, or if the loop body raises a StopIteration exception (which Kernel#loop rescues).
ループ内で定義されたローカル変数がループの外からアクセスされた場合、ループ本体がStopIteration例外を発生させた場合など、動作が変わるようです。
理由1: ループ内で定義した変数のスコープが変わる
loopの場合、ループ内で定義した変数に外部からアクセスすることができず、NameError: undefined local variable or method となりました。
loop do
a = 1
break
end
=> nil
a
Traceback (most recent call last):
.
.
.
NameError (undefined local variable or method `a' for main:Object)
一方 後置while を使う場合は、ループ内で定義した変数に外部からアクセスすることができました。意図せず変数が衝突してしまうケースが考えられそうです。
begin
a = 1
end while false
=> nil
a
=> 1
理由2: StopIterationが発生した際の挙動が変わる
loopの場合、ループ内で StopIteration が発生すると、例外をrescueしてnullを返します。
loop do
raise StopIteration
end
=> nil
後置while の場合は、StopIteration で処理が中断されます。
begin
raise StopIteration
end while false
Traceback (most recent call last):
.
.
.
StopIteration (StopIteration)
StopIterationはどんな場面で発生するのか
StopIterationがそもそもどういった意味を持つ例外なのか分からなかったため、Ruby 2.7.0 リファレンスマニュアル を参照したところ、その名の通りイテレーションを止める際に発生する例外のようです。
実際の挙動を確認するため下記コードを実行してみました。
Enumeratorオブジェクトを生成してnextを繰り返すと、要素がなくなったタイミングでStopIterationが発生しました。
enumerator = [1,2,3].each
=> #
enumerator.next
=> 1
enumerator.next
=> 2
enumerator.next
=> 3
irb(main):117:0> enumerator.next
Traceback (most recent call last):
.
.
.
StopIteration (iteration reached an end)
上記のコードを少し書き換えてloop内で実行してみたところ、StopIterationが返されて処理が正常に終了しました。
enumerator = [1,2,3].each
=> #
loop do
p enumerator.next
end
1
2
3
=> [1, 2, 3]
上記以外にもStopIterationが発生するケースはあるのかもしれないですが、今回確認できた限りだと、Enumeratorクラスのnextメソッドを実行した際に要素がなくなった場合のみでした。
後置whileを使う際の注意点
これまでの検証から、下記注意点を抑えた上で後置whileを使うことができそうです。
ポイント
- 1. ループ内で定義した変数のスコープが変わることを理解する
- 2. StopIterationが発生した際の挙動が変わることを理解する
- 3. チーム内で共通認識が取れていることを確認する
これまで挙動の検証を実施してきましたが、3の共通認識が取れているかが特に大切なポイントになります。
まとめ
以上、今回の記事では後置whileの挙動についてご紹介してきました。
繰り返し処理の書き方ひとつにここまで検証をすることもなかなか無いので、自身の勉強にもなりました。
チーム内での議論の材料になればいいなと考えています。
この記事が何かのお役に立てば幸いです。
Rubyのお仕事に関するご相談
Bageleeの運営会社、palanではRubyに関するお仕事のご相談を無料で承っております。
zoomなどのオンラインミーティング、お電話、貴社への訪問、いずれも可能です。
ぜひお気軽にご相談ください。
この記事は
参考になりましたか?
1
0
関連記事
2021年12月22日
RDBでセットメニューを表現する方法
2021年12月11日
Railsでツリー構造アプリを作ってみた
2021年12月9日
モデルに書いていたメソッドをPOROに切り出してみた!
2021年12月7日
gem cancancanを使ってみた!
2021年11月15日
CustomCopで命名規則を作ってみた
2021年10月29日
clambyを利用したウイルススキャン
簡単に自分で作れるWebAR
「palanAR」はオンラインで簡単に作れるWebAR作成ツールです。WebARとはアプリを使用せずに、Webサイト上でARを体験できる新しい技術です。
palanARへpalanでは一緒に働く仲間を募集しています
正社員や業務委託、アルバイトやインターンなど雇用形態にこだわらず、
ベテランの方から業界未経験の方まで様々なかたのお力をお借りしたいと考えております。
運営メンバー
Eishi Saito 総務
SIerやスタートアップ、フリーランスを経て2016年11月にpalan(旧eishis)を設立。 マーケター・ディレクター・エンジニアなど何でも屋。 COBOLからReactまで色んなことやります。
sasakki デザイナー
アメリカの大学を卒業後、日本、シンガポールでデザイナーとして活動。
やまかわたかし デザイナー
フロントエンドデザイナー。デザインからHTML / CSS、JSの実装を担当しています。最近はReactやReact Nativeをよく触っています。
Sayaka Osanai デザイナー
Sketchだいすきプロダクトデザイナー。シンプルだけどちょっとかわいいデザインが得意。 好きな食べものは生ハムとお寿司とカレーです。
はらた エンジニア
サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています
こぼり ともろう エンジニア
サーバーサイドエンジニア。SIerを経て2019年7月に入社。日々学習しながらRuby on Railsを使った開発を行っています。
ささい エンジニア
フロントエンドエンジニア WebGLとReactが強みと言えるように頑張ってます。
Damien
WebAR/VRの企画・開発をやっています。森に住んでいます。
ゲスト bagelee
かっきー
まりな
suzuki
miyagi
ogawa
雑食デザイナー。UI/UXデザインやコーディング、時々フロントエンドやってます。最近はARも。
いわもと
デザイナーをしています。 好きな食べ物はラーメンです。
taishi kobari
フロントエンドの開発を主に担当してます。Blitz.js好きです。
kubota shogo
サーバーサイドエンジニア。Ruby on Railsを使った開発を行いつつ月500kmほど走っています!
nishi tomoya
aihara
グラフィックデザイナーから、フロントエンドエンジニアになりました。最近はWebAR/VRの開発や、Blender、Unityを触っています。モノづくりとワンコが好きです。
nagao
SIerを経てアプリのエンジニアに。xR業界に興味があり、unityを使って開発をしたりしています。
Kainuma
サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています
sugimoto
asama
ando
iwasawa ayane
oshimo
異業界からやってきたデザイナー。 palanARのUIをメインに担当してます。 これからたくさん吸収していきます!