やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

「制御構造」を読む

 あまり使わなそうだけど知らなかった記法があった。

成果物

情報源

制御構造

  • 条件分岐
    • 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つかな?

RubyC言語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 では false または nil だけが偽で、それ以外は 0 や空文字列も含め全て真です。

 はい。地味に大事。

Ruby では if を繋げるのは elsif であり、else if (C のように)でも elif(sh のように)でもないことに注意してください。

 まったく困ったものだ。冗長だし書きたくない。

また if の条件式が正規表現リテラルである時には特別に

$_ =~ リテラル

であるかのように評価されます。

 は? $_ってなに?

最後に 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 節の本体を評価します。

つまり、

case0
when1, 式2
  stmt1
when3, 式4
  stmt2
else
  stmt3
end

は以下の if 式とほぼ等価です。

_tmp = 式0
if1 === _tmp or2 === _tmp
  stmt1
elsif3 === _tmp or4 === _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 ...  in1 [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 はもっとも内側のループを脱出します。ループとは

のいずれかを指します。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 が発生します。

 retryrescue節でしか使えない。ここ大事。

イテレータ呼び出しにおける break, next, redo, retry をまとめると以下のようになります。

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 rescue2

式1で例外が発生したとき、式2を評価します。以下と同じ意味です。捕捉する例外クラスを指定することはできません。 (つまり、StandardError 例外クラスのサブクラスだけしか捕捉できません)

 rescueにも修飾子があったのか。

begin1
rescue2
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 {...}って、おもいっきりブロック付きメソッド呼び出しの形だね。そういうことだったのか? ENDBEGINはブロック付きメソッド呼出だったのかな? だからそれと同じスコープだってことかな?

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"

 ふーん。

所感

 細かい挙動をみると混乱しそう。

対象環境

$ uname -a
Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux