やってみる

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

「Ruby プログラムの実行」を読む8(メソッド)

 メソッド本体にrescueとか書けたんだね。知らんかったよ。

成果物

情報源

メソッド

メソッドの呼出

まずレシーバ式を評価してレシーバとなるオブジェクトを得ます。レシーバ式が省略された場合は呼び出しを行っているブロックのself がレシーバです。

続いて引数式を左から右の順番で評価し、レシーバに対してメソッドの検索を行います。検索が失敗したら例外 NameError を発生、成功したらメソッドを実行します。

 そういえばRubyってオーバーロードはできるのか? オーバーロードとは、同名であり異なる引数をもったメソッドを定義すること。

 ググってみたらできないってさ。言語仕様的にはできないけど、ライブラリを使えばできると。

 でもそのコードがダサい。

gem install overloader
require 'overloader'
class A
  extend Overloader
  overload do
    def m; :m1; end
    def m(x); :m2; end
    def m(x, y); :m3; end
  end
end
  • orverloadって3回も書かなきゃいけない
    • require
    • extend
    • overload do
  • overload doという文でネストされてしまう
    • ほかのメソッドと同じように書きたい

 ふつうにやると単にメソッドが上書きされるだけでオーバーロードできない。

class A
  def m; :m1; end
  def m(x); :m2; end
  def m(x, y); :m3; end
end
A.new.m #=>wrong number of arguments (given 0, expected 2) (ArgumentError)

 ググったら3.0からできそうな雰囲気?

a.rb

class A
  def m; :m1; end
  def m(x); :m2; end
  def m(x, y); :m3; end
end
typeprof a.rb > a.rbs
cat a.rbs
# TypeProf 0.12.0

# Classes
class A
  def m: (untyped x) -> :m2
       | (untyped x, untyped y) -> :m3
       | -> :m1
end

 このあとはsteepというgemを使って静的型チェックする、とあった。

 あれ、実行はできないの? 書いていないなので、できないっぽい。えー。じゃあオーバーロードできないってことかい。なんだよー先に言ってくれよ。

 がっかり。

 ドキュメントに戻ろう。

またメソッドを実行する際にはブロックを与えることが可能です。メソッドに対してブロックを与えると、そのメソッドの実行中にyield が実行されたときにはじめてブロックが評価されます。yield されなかったときはブロックは単に無視され、実行されません。

def m(&b); yield b; end
m {p 'BLOCK'}

 このコードって超わかりにくいわ。ブロックというクロージャと、yieldというイテレータとがつながらない。とくにほかのプログラミング言語をやっていると違和感ハンパない。

メソッドにブロックを与える場合、そのブロックはメソッドを呼び出す側のブロックの self と class を継承します。Module#module_eval/class_eval、 BasicObject#instance_eval の三つだけが例外で、以下のように変更されます。

メソッド self class
Module#module_eval そのレシーバ そのレシーバ
Module#class_eval そのレシーバ そのレシーバ
BasicObject#instance_eval そのレシーバ そのレシーバの特異クラス

 はあ。よくわからんけども。

eval

eval の第二引数に Proc オブジェクトまたは Binding オブジェクトを与えたときは、その生成時のブロックのうえで文字列を評価します。

eval "p 'P'"

 Procを渡すと怒られた。使い方知らんし。まあいいや。放置で。

eval "p 'P'" Proc.new
syntax error, unexpected constant, expecting end-of-input (SyntaxError)

メソッドの実行

メソッドの実行はフレームのうえにひとつだけブロックがある状態で開始します。このブロックを以下、仮にトップレベルブロックと呼んでおきます。トップレベルブロックの self はレシーバで、class は定義されていません。

 フレームってなんぞ? よくわからんけども読み進める。

まず、省略不可能な引数が存在するなら、与えられた値をトップレベルブロックのローカル変数に代入します。

 は? えーと、省略可能な引数って、キーワード引数のことかな? ローカル変数というのはキーワード引数の変数名のこと?

p = 'P'
def m(p='') # ここで上のp='P'に''を代入するって言っているように見えるんですけど、そんなわけないよね?
end

省略可能な引数が存在し、実際に省略されていたら、トップレベルブロック上でデフォルト式を評価し、その値をトップレベルブロックのローカル変数に代入します。実際には省略されなかったら、与えられた値を同じくローカル変数に代入します。

 たぶんキーワード引数の説明だろう。

*args の形のパラメータ指定があるなら、残りの引数すべてを配列としてローカル変数に代入します。

 可変長引数の説明だね。はいはい。

さらに、ブロック引数 blockvar が存在するならば、メソッドに与えられたブロックを Proc オブジェクト化してトップレベルブロック上のローカル変数 blockvar に代入します。ブロックが与えられていないなら nil を代入します。

 ブロック引数の説明だね。はいはい。

続いて本体が評価され、メソッドレベルの rescue 節または else 節が評価され、最後に ensure 節が評価されます。

 メソッドレベルのrescue節ってどんなコード? まさか以下のように書けるの?

def m
  raise
rescue => e
  p e
ensure
  p '終了'
end
m
RuntimeError
"終了"
=> RuntimeError

 えええ! できたよ。マジか。知らんかった。ちゃんとコード例を出してくれや。

 そういえばJavaはこういう書き方できたっけ? 忘れた。

 ふつうは以下だと思うけど。これでも同じ結果になる。

def m
  begin
    raise
  rescue => e
    p e
  ensure
    p '終了'
  end
end
m
RuntimeError
"終了"
=> RuntimeError

メソッド全体の値は return に渡した値です。 return が呼び出されなかった場合は、本体/rescue/else の中で最後に評価した式の値です。その三つともすべてがカラだった場合は nil になります。

 つまりreturnがあったらたとえrescueelseensureがあっても戻り値はreturnのやつを使うってことかな? 試してみよう。

def m
  return 'RETURN'
  raise
rescue => e
  p e
  'RESCUE'
ensure
  p '終了'
  'ENSURE'
end
m
"終了"
=> "RETURN"

 ほーう。こうなるのか。つまりreturnによってその時点でメソッド呼出元に戻される。なのでraiseされずrescueされないと。でもensureだけはその名のとおり実行を保障されると。でもensureが最後に実行されたにもかかわらず、戻り値はreturnで指定したものだと。これまでのルールでは、最後に実行した式の戻り値が返るはず。それだと'ENSURE'が返るはず。でもreturnで指定した'RETURN'が返った。return指定すると戻り値の値を指定するという動作になるってことか。

 returnしなかったら?

def m
  'RETURN'
  raise
rescue => e
  p e
  'RESCUE'
ensure
  p '終了'
  'ENSURE'
end
m
"終了"
=> "RESCUE"

 え、RESCUEなの? てっきりENSUREかと思っていたんだが。これは予想外。

 つまりensureは必ず実行されることを保証する。けれど戻り値については「最後に実行した式の戻り値を返す」というルールに当てはまらない。ということか。

 これは難しい。ちょっとまとめよう。

 戻り値は次のうち最初にみつかったものに決定される。

  1. return指定された値
  2. 最後に実行した式の戻り値
    1. ただしensureは対象外

その三つともすべてがカラだった場合は nil になります。

 といっているが、メソッドの戻り値がnilになることなんてあるの? だってメソッド定義式のところで以下のように言っているよ?

メソッド定義式は、メソッド名を Symbol にしたオブジェクトを返します。

ためしてみる。あれ、nilが返ってきたぞ?

def m; end
m #=> nil

 あー、もしかしてこういうこと?

a = def m; end
a #=> :m

 やっぱり。はいはい。def式自体の戻り値がシンボルだって話だったのね。

ブロック付きメソッド呼び出し

メソッドに対してブロックが与えられていたらそのメソッドをブロック付きメソッドと呼びます。ブロック付きメソッドからは yield によって与えられたブロックに実行を移すことができます。

def m(&block)
  yield block
end
m {p 'BLOCK'}
"BLOCK"
=> "BLOCK"

 mはブロック付きメソッドである。

 というか、ブロック引数&blockがなくても動作してしまったんだが?

def m
  yield
end
m {p 'BLOCK'}
"BLOCK"
=> "BLOCK"

 そういうもんなの? 

 どういうときに使うんだろう。処理を一部だけブロックのものにしたいとき? でもメソッド本体側ではyieldがあるからブロックありきなんだよね。

 もしブロックが渡されなかったらどうなる?

def m
  yield
end
m
no block given (yield) (LocalJumpError)

 怒られたよ。ふーん。

 ブロック引数があるのとないのとではどう使い分けるんだ? メソッド本体で複数の異なるブロックのうち条件分岐で使い分けたいときにはブロック引数をつける、とか? あるいは単純に、複数のブロックを受け取りたいときとか。

ブロック引数をとることができます。

 先述のとおり。&blockのことだと思われる。

break … ブロックがスタックフレーム上にあるなら、そのフレームのブロックの直後にジャンプします。ブロック付きメソッドを break して終了したらその値は nil です。スタックフレーム上にないなら例外 LocalJumpError を発生します。

 C言語breakと同じだと思う。

next ブロックの終わりまでジャンプ

 nextC言語でいうcontinueと同じっぽい。

retry 複雑だ…

 は? え、なにその感想。ちょっとまって。確認させて。retryというキーワードがあるってこと? すでにそこからわからんのですが。

def m(times: 3)
  try = 0
  begin
    try += 1
    yield
  rescue
    retry if try < times
    raise
  end
end
m { p '実行' }
"実行"
=> "実行"

 あれ、1回しか実行されてないやん。どゆこと? てっきり3回実行と表示されるかと思ったのに。

 わからん。

eval, instance_eval, module_eval

これなんだっけ

 知らんがな。こっちが聞きたいわ。

 ようするに、その文脈でevalするってことね。

対象環境