あまり使わなそうだけど知らなかった記法があった。
成果物
情報源
制御構造
- 条件分岐
- if
- unless
- case
- 繰り返し
- while
- until
- for
- break
- next
- redo
- retry
- 例外処理
- raise
- begin
- その他
- return
- BEGIN
- END
Rubyでは(Cなどとは異なり)制御構造は式であって、何らかの値を返すものがあります(返さないものもあります。値を返さない式を代入式の右辺に置くと syntax error になります)。
たぶん返さないのはnext
,redo
,retry
,BEGIN
,END
の5つかな?
RubyはC言語やPerlから引き継いだ制御構造を持ちますが、その他にメソッド呼び出し(super・ブロック付き・yield)/ブロック付きメソッド呼び出しという制御構造の抽象化を援助する機能があります。ブロック付きメソッド呼び出しは繰り返しを始めとする制御構造をクラス設計者が定義する事が出来るものです.
条件分岐
if
例:
if age >= 12 then print "adult fee\n" else print "child fee\n" end gender = if foo.gender == "male" then "male" else "female" end
文法:
if 式 [then] 式 ... [elsif 式 [then] 式 ... ] ... [else 式 ... ] end
条件式を評価した結果が真である時、then 以下の式を評価します。 if の条件式が偽であれば elsif の条件を評価します。 elsif 節は複数指定でき、全ての if および elsif の条件式が偽であったとき else 節があればその式が評価されます。
上から順に条件一致するものを探す。そのうちどれかひとつだけを通る。またはどれも通らない。
if 式は、条件が成立した節(あるいは else 節)の最後に評価した式の結果を返します。else 節がなくいずれの条件も成り立たなければ nil を返します。
必ず値を返す。
はい。地味に大事。
Ruby では if を繋げるのは elsif であり、else if (C のように)でも elif(sh のように)でもないことに注意してください。
まったく困ったものだ。冗長だし書きたくない。
$_ =~ リテラル
であるかのように評価されます。
は? $_
ってなに?
最後に Kernel.#gets または Kernel.#readline で読み込んだ文字列です。
標準入力値ってことか?
a.rb
p $_ if /ruby/
echo 'ruby' | ruby a.rb
a.rb:1: warning: regex literal in condition
なんか警告でたんですけど? 一致してないっぽいんですけど? どゆこと? わかんねーよ。ちゃんと動くコード例を示してくれや!
if 修飾子
例:
print "debug\n" if $DEBUG
文法:
式 if 式
右辺の条件が成立する時に、左辺の式を評価してその結果を返します。条件が成立しなければ nil を返します。
unless
if
の真偽値逆転形。
例:
unless baby? feed_meat else feed_milk end
文法:
unless 式 [then] 式 ... [else 式 ... ] end
unless は if と反対で、条件式が偽の時に then 以下の式を評価します。unless 式にelsif を指定することはできません。
elsif
が使えない罠。
unless 修飾子
例:
print "stop\n" unless valid(passwd)
文法:
式 unless 式
右辺の条件が成立しない時に、左辺の式を評価してその結果を返します。条件が成立すれば nil を返します。
case
例:
case $age when 0 .. 2 "baby" when 3 .. 6 "little child" when 7 .. 12 "child" when 13 .. 18 "youth" else "adult" end
文法:
case [式] [when 式 [, 式] ...[, `*' 式] [then] 式..].. [when `*' 式 [then] 式..].. [else 式..] end
case は一つの式に対する一致判定による分岐を行います。when 節で指定された値と最初の式を評価した結果とを演算子 === を用いて比較して、一致する場合には when 節の本体を評価します。
つまり、
case 式0 when 式1, 式2 stmt1 when 式3, 式4 stmt2 else stmt3 end
は以下の if 式とほぼ等価です。
_tmp = 式0 if 式1 === _tmp or 式2 === _tmp stmt1 elsif 式3 === _tmp or 式4 === _tmp stmt2 else stmt3 end
when 節の評価順序はこの上記 if 文に書き直した場合と同じです。つまり上から順に(そして左から順に) === が評価されます。また「式0」は1回だけ評価されます。
when 節の最後の式に `*' を前置すればその式は配列展開されます。
ary = [1,2,3] case v when *ary .. end
は、
case v when 1, 2, 3 .. end
と等価です。
また === がどのような条件で真になるかは、各クラスの === メソッドの動作についてのドキュメントを参照して下さい。
リンクがないのでググった。読んだら==
の別名って書いてたんですけど。本当? オブジェクトIDが同一であるか、値が同じかの違いがあるんじゃなかったっけ?
case の「式」を省略した場合、when の条件式が偽でない最初の式を評価します。
foo = false bar = true quu = false case when foo then puts 'foo is true' when bar then puts 'bar is true' when quu then puts 'quu is true' end # "bar is true"と表示される
case は、条件が成立した when 節、(あるいは else 節) の最後に評価した式の結果を返します。いずれの条件も成り立たなければ nil を返します。
C言語のswitch
文だったらbreak
するまで継続していた。それとはまったくことなるルートをたどる。if
文のように条件が一致したらreturn
する感じ。
このドキュメントにはパターンマッチcase
-in
について書いてない。そっちはさらに書き方がたくさんあって難しい。
繰り返し
while
例:
ary = [0,2,4,8,16,32,64,128,256,512,1024] i = 0 while i < ary.length print ary[i] i += 1 end
文法:
while 式 [do] ... end
式を評価した値が真の間、本体を繰り返し実行します。
while は nil を返します。また、引数を伴った break により while 式の戻り値をその値にすることもできます。
え、break
ってreturn
みたく戻り値を指定できるの?!
試してみよう。
x = while true break 7 end p x #=> 7
おお、指定できた。
while 修飾子
例:
sleep(60) while io_not_ready?
文法:
式 while 式
右辺の式を評価した値が真の間、左辺を繰り返し実行します。
左辺の式が begin 節である場合にはそれを最初に一回評価してから繰り返します。
例:
send_request(data) begin res = get_response() end while res == 'Continue'
C言語でいうdo
-while
文のことか。最初の1回だけは必ず実行して、それ以降は条件一致している間だけループする。
でもこれ、読みづらいな。書きたくない。これは書いてて楽しくないわ。
while 修飾した式は nil を返します。また、引数を伴った break により while 修飾した式の戻り値をその値にすることもできます。
until
while
の真偽値逆転形。
例:
until f.eof? print f.gets end
文法:
until 式 [do] ... end
式を評価した値が真になるまで、本体を繰り返して実行します。
until は nil を返します。また、引数を伴った break により until 式の戻り値をその値にすることもできます。
until修飾子
例:
print(f.gets) until f.eof?
文法:
式 until 式
右辺の式を評価した値が真になるまで、左辺を繰り返して実行します。
左辺の式が begin 節である場合にはそれを最初に一回評価してから繰り返します。
例:
send_request(data) begin res = get_response() end until res == 'OK'
until 修飾した式は nil を返します。また、引数を伴った break により until 修飾した式の戻り値をその値にすることもできます。
do
-while
の真偽値逆転形。
for
例:
for i in [1, 2, 3] print i*2, "\n" end
文法:
for lhs ... in 式1 [do] 式2.. end
式を評価した結果のオブジェクトの各要素に対して本体を繰り返して実行します。これは以下の式とほぼ等価です。
(式1).each `{' `|' lhs..`|' 式2.. `}'
「ほぼ」というのは、do ... endまたは{ }によるブロックは新しいローカル変数の有効範囲を導入するのに対し、 for文はローカル変数のスコープに影響を及ぼさない点が異なるからです。
だからfor
文よりeach
のほうがよく使われるのだろう。
for は、in に指定したオブジェクトの each メソッドの戻り値を返します。
複数のループ変数指定は以下のような場合に使用します。
for i,j in [[1,2], [3,4], [5,6]] p [i,j] end => [1, 2] [3, 4] [5, 6]
for や each で配列要素を複数個ずつ取得しながらループすることはできません。
for i,j in [1, 2, 3] p [i,j] end => [1, nil] [2, nil] [3, nil] # [1,2] [3,nil] を期待するかもしれないがそうはならない
代わりにそのようなメソッド(イテレータ)を定義する必要があります。
class Array def each2 i = 0 while i < self.size yield self[i], self[i+1] i += 2 end end end
まあそんな実装することないだろうけども。
break
例:
i = 0 while i < 3 print i, "\n" break end
文法:
break break val
break はもっとも内側のループを脱出します。ループとは
- while
- until
- for
- イテレータ
のいずれかを指します。C 言語と異なり、break はループを脱出する作用だけを持ち、case を抜ける作用は持ちません。
break によりループを抜けた for やイテレータは nil を返します。ただし、引数を指定した場合はループの戻り値はその引数になります。
C言語と違って戻り値を指定できる。
next
例:
# 空行を捨てるcat ARGF.each_line do |line| next if line.strip.empty? print line end
文法:
next next val
nextはもっとも内側のループの次の繰り返しにジャンプします。イテレータでは、yield 呼び出しの脱出になります。
next により抜けた yield 式は nil を返します。ただし、引数を指定した場合、yield 式の戻り値はその引数になります。
C言語でいうcontinue
相当。
redo
例:
redo
文法:
redo
ループ条件のチェックを行なわず、現在の繰り返しをやり直します。
インデックス値はそのまま。無限ループになってしまいうる。
retry
例:
retry
文法:
retry
retry は、rescue 節で begin 式をはじめからもう一度実行するのに使用します。 retry を使うことである処理が成功するまで処理を繰り返すようなループを作ることができます。
begin do_something # exception raised rescue # handles error retry # restart from beginning end
rescue 節以外で retry が用いられた場合には例外 SyntaxError が発生します。
retry
はrescue
節でしか使えない。ここ大事。
def iter (a) : (b) yield (c) : (d) end iter { redo } -> (b) へ飛ぶ iter { next } -> (c) へ飛ぶ iter { break } -> (d) へ飛ぶ
(a) は、厳密には引数評価から始まります。(b) はブロック実行の直前を指しています(yield の引数が再評価されるわけではない)。(d) は、メソッドの終了です。
def iter(var = p("(a)")) yield p "(c)" ensure p "(d)" end iter { p "(b)"; redo } # -> (a) .. (b)(b)(b)(b) ... iter { p "(b)"; next } # -> (a) .. (b)(c) .. (d) iter { p "(b)"; break } # -> (a)..(b)(d)
redo
は無限ループじゃんか。やめてくれ。
例外処理
raise
例:
raise "you lose" # 例外 RuntimeError を発生させる # 以下の二つは SyntaxError を発生させる raise SyntaxError, "invalid syntax" raise SyntaxError.new("invalid syntax") raise # 最後の例外の再発生
文法:
raise raise messageまたはexception raise error_type, message raise error_type, message, traceback
コンストラクタの引数とはカンマ区切りなのか。
例外を発生させます。第一の形式では直前の例外を再発生させます。第二の形式では、引数が文字列であった場合、その文字列をメッセージとする RuntimeError 例外を発生させます。引数が例外オブジェクトであった場合にはその例外を発生させます。第三の形式では第一引数で指定された例外を、第二引数をメッセージとして発生させます。第四の形式の第三引数は $@または Kernel.#callerで得られるスタック情報で、例外が発生した場所を示します。
発生した例外は後述の begin 式の rescue 節で捕らえることができます。その場合 rescue error_type => var の形式を使えば例外オブジェクトを得られます。このオブジェクトは組み込み変数 $! でも得られます。また例外が発生したソースコード上の位置は変数 $@ に格納されます。
Kernel.#raise は Ruby の予約語ではなく、Kernel モジュールで定義されている関数的メソッドです。
はあ? Rubyの予約語ではないだと?! 関数的メソッドってなに? モジュール関数じゃないの?
begin
例:
begin do_something rescue recover ensure must_to_do end
文法:
begin 式.. [rescue [error_type,..] [=> evar] [then] 式..].. [else 式..] [ensure 式..] end
本体の実行中に例外が発生した場合、rescue 節(複数指定できます)が与えられていれば例外を捕捉できます。発生した例外と一致する rescue 節が存在する時には rescue 節の本体が実行されます。発生した例外は $! を使って参照することができます。また、指定されていれば変数 evar にも $! と同様に発生した例外が格納されます。
begin raise "error message" rescue => evar p $! p evar end # => #<RuntimeError: error message> #<RuntimeError: error message>
例外の一致判定は,発生した例外が rescue 節で指定したクラスのインスタンスであるかどうかで行われます。
例外の種別ごとにルート分岐できるってことか。
error_type が省略された時は StandardError のサブクラスである全ての例外を捕捉します。Rubyの組み込み例外は(SystemExit や Interrupt のような脱出を目的としたものを除いて) StandardError のサブクラスです。
StandardErrorがすべての例外の親玉。ということでいいんだよね? 罠ないよね? じつは自作例外はそれを継承しなくても作れちゃって、その場合はキャッチできないとかいう罠があったりしないよね?
例外クラスのクラス階層については Builtin libraries を参照してください。
rescue では error_type は通常の引数と同じように評価され、そのいずれかが一致すれば本体が実行されます。error_type を評価した値がクラスやモジュールでない場合には例外 TypeError が発生します。
省略可能な else 節は、本体の実行によって例外が発生しなかった場合に評価されます。
ensure
とも違う。節が多すぎるよ。
ensure 節が存在する時は begin 式を終了する直前に必ず ensure 節の本体を評価します。
他の言語でいうfinally
。
begin式全体の評価値は、本体/rescue節/else節のうち最後に評価された文の値です。また各節において文が存在しなかったときの値はnilです。いずれにしてもensure節の値は無視されます。
ensure
節の値は無視される。ここポイント。
クラス/メソッドの定義/クラス定義、クラス/メソッドの定義/モジュール定義、クラス/メソッドの定義/メソッド定義 などの定義文では、それぞれ begin なしで rescue, ensure 節を定義でき、これにより例外を処理することができます。
以下のように書ける。
def m :m rescue p 'rescue' ensure p 'ensure' end
いつ使うかわからんけど。
rescue修飾子
例:
open("nonexistent file") rescue STDERR.puts "Warning: #$!"
文法:
式1 rescue 式2
式1で例外が発生したとき、式2を評価します。以下と同じ意味です。捕捉する例外クラスを指定することはできません。 (つまり、StandardError 例外クラスのサブクラスだけしか捕捉できません)
rescue
にも修飾子があったのか。
begin 式1 rescue 式2 end
rescue修飾子を伴う式の値は例外が発生しなければ式1、例外が発生すれば式2 です。
var = open("nonexistent file") rescue false p var => false
ただし、優先順位の都合により式全体を括弧で囲む必要がある場合があります。メソッドの引数にするには二重の括弧が必要です。
p(open("nonexistent file") rescue false) => parse error p((open("nonexistent file") rescue false)) => false
例外を握りつぶすときに便利そう。
その他
return
例:
return return 12 return 1,2,3
文法:
return [式[`,' 式 ... ]]
式の値を戻り値としてメソッドの実行を終了します。式が2つ以上与えられた時には、それらを要素とする配列をメソッドの戻り値とします。式が省略された場合には nil を戻り値とします。
トップレベルで return した場合はプログラムが終了します。 require, load されたファイル内のトップレベルで return した場合は呼び出し元に返ります。
トップレベルにも書けたのか。まあ使うことはないだろう。
BEGIN
例:
BEGIN {
...
}
文法:
BEGIN '{' 文.. '}'
初期化ルーチンを登録します。BEGINブロックで指定した文は当該ファイルのどの文が実行されるより前に実行されます。複数のBEGINが指定された場合には指定された順に実行されます。
複数指定できたのか。
BEGIN { p 'BEGIN 1' } BEGIN { p 'BEGIN 2' } BEGIN { p 'BEGIN 3' }
"BEGIN 1" "BEGIN 2" "BEGIN 3"
BEGINブロックはコンパイル時に登録されます。 BEGIN ブロックは、独立したローカル変数のスコープを導入しません。つまり、 BEGIN ブロック内で定義したローカル変数は BEGIN ブロックを抜けた後も使用可能です。
それグローバル変数に似ているね。罠になる? いや問題ないか。たぶん。代入式で初期化されるだろうから。
BEGINはトップレベル以外では書けません。全て SyntaxErrorになります。
def foo BEGIN { p "begin" } end # => -e:2: syntax error, unexpected keyword_BEGIN class Foo BEGIN { p "begin" } end # => -e:2: syntax error, unexpected keyword_BEGIN loop do BEGIN { p "begin" } end # => -e:2: syntax error, unexpected keyword_BEGIN
BEGIN
はトップレベルに書くべし。
でも、そもそもBEGIN
,END
はライブラリのときは書かないほうがいいよね。実行ファイルのときルートファイルのみ記載するとか、制限しておかないと大変なことになりそう。複数ファイルのいたる場所でBEGIN
が書いてあって、各ファイルをrequire
する順序によって実行順が変わる。そんな悲惨なことになりかねない。
END
例:
END {
...
}
文法:
END '{' 文.. '}'
「後始末」ルーチンを登録します。END ブロックで指定した文はインタプリタが終了する時に実行されます。Ruby の終了時処理について詳しくは 終了処理を参照してください。
複数の END ブロックを登録した場合は、登録したときと逆の順序で実行されます。
END { p 1 } END { p 2 } END { p 3 } # => 3 2 1
なにこれ、わかりづらい。
END ブロックは一つの記述につき最初の一回のみ有効です。たとえば以下のようにループの中で実行しても複数の END ブロックが登録されるわけではありません。そのような目的には Kernel.#at_exit を使います。
5.times do |i| END { p i } end # => 0 END をメソッド定義式中に書くと警告が出ます。意図的にこのようなことを行いたい場合は Kernel.#at_exit を使います。 def foo END { p "end" } end p foo # => -:2: warning: END in method; use at_exit nil "end"
そんな実装はしないと思う。すべきでもないと思う。でも構文的には書けてしまうから説明している感じかな?
END は、BEGIN とは異なり実行時に後処理を登録します。したがって、以下の例では END ブロックは実行されません。
if false END { p "end" } end
BEGIN
ブロックだったら実行するのか。と思ったらエラーになった。END
はエラーにならず上記のように書ける。
BEGIN { p 'BEGIN 0' } if false # syntax error, unexpected `if' modifier, expecting end-of-input
if false BEGIN { p 'BEGIN 0' } # BEGIN is permitted only at toplevel end
なんて紛らわしいんだ。統一性のない変な書き方ができてしまう。Rubyってそういうの多いな。楽しいときもあれば罠になったり混乱することも多そうだ。
END や Kernel.#at_exit で登録した後処理を取り消すことはできません。
というか一度定義したBEGIN
,END
はどれもどうやっても削除できないってことなのでは? それとも削除する方法はあるの?
END ブロックは周囲とスコープを共有します。すなわちイテレータと同様のスコープを持ちます。
はあ、イテレータと同じ? あー、もしかしてfor
文のことか? つまりBEGIN
と同じってことかな? END
ブロック内で定義したローカル変数は、END
ブロック外でもローカル変数として使えると。そういうことで合ってる?
BEGIN { begin_local = 111 } END { end_local = 222 } p "begin_local=#{begin_local}" #=> "begin_local=111" p "end_local=#{end_local}" #=> "end_local="
あれ、END
ブロック内ローカル変数が外で参照できていない? でもちょっとまって。ローカル変数って未定義のときはエラーになったよね?
> p end_local (irb):1:in `<main>': undefined local variable or method `end_local' for main:Object (NameError)
"end_local="
ということは空値が返ってきたってことか? それとも値がnil
だと出力するとき空文字になる? 後者っぽい。
> a = nil > p "#{a}" ""
たぶんEND {...}
で定義を読み込んだときにローカル変数が宣言されて値がnil
で初期化されたんだろう。でもまだプログラム終端ではないためEND
ブロック内の処理である代入は実行されない。だから終端前に参照されたローカル変数end_local
の値はnil
だった。ということかな?
それって事実上、ブロック外で使えないってことじゃね?
そういうことは最初にいってくれ。
END
ブロックではローカル変数が宣言できる。でも代入処理はプログラム終了時に行われるため、値がnil
で初期化されたローカル変数が用意される。エラーにはならないけど、値は代入されていない。有意義な使い方はできない。そういうことだよね。
でも「イテレータと同様のスコープ」という言い回しが気になる。もしかしてイテレータって、ブロック付きメソッド呼出のことを言っている? そのときyield
で呼び出されるブロック内で宣言された変数のスコープと同じだってことか?
そういえばEND {...}
って、おもいっきりブロック付きメソッド呼び出しの形だね。そういうことだったのか? END
やBEGIN
はブロック付きメソッド呼出だったのかな? だからそれと同じスコープだってことかな?
def m v = 0 yield p v end m { p v; v=123; } # undefined local variable or method `v' for main:Object (NameError)
あれ、怒られた。
def m v = 0 yield p v end m { v=123; } #=> 0 p v # undefined local variable or method `v' for main:Object (NameError)
うーん、ふつうのブロック付きメソッドのブロック内でローカル変数を宣言してもその外側では参照できないっぽい。この動作に疑問はない。ただ、このドキュメントの説明文がよくわからん。「イテレータと同様のスコープ」っていうのが、結局どういうことなのかわからん。まあいいや。放置で。
END ブロックの中で発生した例外はその END ブロックを中断しますが、すべての後始末ルーチンが実行されるよう、インタプリタは終了せずにメッセージだけを出力します。
例:
END { p "FOO" } END { raise "bar"; p "BAR" } END { raise "baz"; p "BAZ" } => baz (RuntimeError) bar (RuntimeError) "FOO"
ふーん。
所感
細かい挙動をみると混乱しそう。
対象環境
- Raspbierry pi 4 Model B
- Raspberry Pi OS buster 10.0 2020-08-20 ※
- bash 5.0.3(1)-release
- Ruby 3.0.2
$ uname -a Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux