やってみる

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

「メソッド呼び出し(super・ブロック付き・yield)」を読む

 superはオーバーライド元のメソッド呼出に使う。

成果物

情報源

メソッド呼び出し(super・ブロック付き・yield)

  • super
  • ブロック付きメソッド呼び出し
  • yield
  • ブロックパラメータの挙動
  • .() および ::() 形式のメソッド呼び出し(callメソッドの糖衣構文)

例:

foo.bar()
foo.bar
bar()
print "hello world\n"
print
Class.new
Class::new

文法:

[式  `.'] 識別子 [`(' [[`*'] 式] ... [`&' 式] `)']
[式 `::'] 識別子 [`(' [[`*'] 式] ... [`&' 式] `)']

メソッド呼び出し式はレシーバ(`.' の左側の式の値)のメソッドを呼び出します。レシーバが指定されない時は self のメソッドを呼び出します。

 selfの説明をしてくれよ。インスタンスってことでいいよね? あとselfのメソッドを呼び出すのはクラス内で定義したメソッド内のとき限定の話だよね?

class A
  def m
    p n
    p self.n
  end
  private
  def n
    :n
  end
end
A.new.m

 と思って試してみたら、クラス外でのselfmainというインスタンスを指すようだ。なにそれ。

p self #=> main
def m
  p self #=> main
end
m

 でもメソッド定義するときのselfはクラスを意味するっぽい。ので混乱する。そこんところちゃんと説明してほしい。

class A
  def self.class_method
    :class_method
  end
  def ins_method
    :ins_method
  end
end
A::class_method
A.class_method
#A::ins_method #=> undefined method `ins_method' for A:Class (NoMethodError)
#A.ins_method #=> undefined method `ins_method' for A:Class (NoMethodError)
#A.new::class_method #=> undefined method `class_method' for #<A:0x02094690> (NoMethodError)
#A.new.class_method #=> undefined method `class_method' for #<A:0x015846a8> (NoMethodError)
A.new::ins_method
A.new.ins_method

.' と::' とはほぼ同じ意味です。

 .::はほぼ同じ意味らしい。たしかに先述のコードではインスタンスメソッド、クラスメソッドどちらも.::で呼び出せた。

但し、定数を表す場合は、 `::' を使わなければいけません(例: Math::PI)。逆に、

Klass::Foo

とした場合、常に定数と見なされるという制限があります。 `::' を、クラスメソッド呼び出しに使うという流儀がありますが、この点は要注意です。大文字で始まるメソッド名を使用する場合は

Klass.Foo

と `.' を使うか、

Klass::Foo()

と括弧でメソッド呼び出しであることを明示する必要があります。

 定数とクラスメソッド、同名のものを用意してそれぞれの方法で呼んでみた。

class C
  Max = 111
  def self.Max
    :Max
  end
end
p C::Max   #=> 111
p C.Max    #=> :Max
p C::Max() #=> :Max
p C.Max()  #=> :Max

 命名規則の話になる。いくつもの方法があって、どれが最善か考えることになりそう。

 これまでの情報をまとめよう。

  • 定義
    • 定数は頭文字が大文字の変数である
    • メソッドは頭文字を大文字にすることもできる
  • 呼出
    • 頭文字が大文字で::のとき定数であるとみなす
    • 頭文字が大文字で::のとき末尾に()があればメソッドであるとみなす
強制 記号 解釈
制限 ::Const 定数
流儀 ::Method() クラスメソッド呼出
流儀 .Method クラスメソッド呼出

 C++など他のプログラミング言語ではクラスメソッドを呼び出すときClass::Methodのように.でなく::にすることがある。しかしRubyでは::が使われたら定数であるとみなす。むしろ定数を参照するときは必ず::でなければならない。

 だとしたらメソッド呼出は必ず.にしたほうが良さそうか? いや、それだとクラスメソッドかインスタンスメソッド、どちらなのか見分けづらくなってしまう。最適なのは以下だと思われる。

名前 部品 スコープ演算子 case
ins.ins_method インスタンスメソッド . snake_case
Cls::cls_method クラスメソッド :: snake_case
Cls::CONST 定数 :: SNAKE_CASE

 定数とクラスメソッドの名前重複が起こらないようにした。

  • 定義
    • ファイル名はsnake_case
    • クラス、モジュール名はCamelCase
    • メソッド名はsnake_case
    • 定数はSNAKE_CASE
  • 呼出
    • インスタンスメソッドは.で呼ぶ
    • それ以外は::で呼ぶ(定数、クラスメソッド、モジュール関数)

 もしクラスメソッドをキャメルケースにしたら覚えにくくなる。

 Rubyは頭文字を大文字にすると定数になる。その制約だとキャメルケースCamelCaseにも対応できてしまう。それだとクラス名と同じになり見分けがつかない。また、メソッド名は制約がないためキャメルケースにもできてしまう。やはり見分けがつかなくなる。そこで定数はすべて大文字のスネークケースにする。これで文字種だけで定数かそれ以外であると識別できる。

 ::は一貫してトップスコープ、クラススコープ、モジュールスコープを指すときに使う。インスタンスメソッド呼出は必ず.で呼ぶ。あるいはクラス定義内におけるメソッドの中にあるself.は省略したほうが短く書けて良い。

 ちなみにセッター、ゲッターに関しては@がないインスタンス 変数名のメソッド名になる。attr,attr_reader,attr_writerの糖衣構文によってその名前のメソッドが自動生成されるため、それに倣う形だ。C#だったらそれらプロパティはキャメルケースになるが、Rubyではスネークケースである。また、プロパティという種別ではなくメソッドである。

class B
  attr_accessor :name
  def initialize
    @name = 'ytyaru'
  end
end
b = B.new
p b.name #=> 'ytyaru'
b.name = 'YTYARU'
p b.name #=> 'YTYARU'

メソッド名には通常の識別子の他、識別子に ? または ! の続いたものが許されます。慣習として、述語(真偽値を返すメソッド)には ? を、同名の(! の無い)メソッドに比べてより破壊的な作用をもつメソッド(例: tr と tr!)には ! をつけるようになっています。

 地味に大事。

引数の直前に * がついている場合、その引数の値が展開されて渡されます。展開はメソッド to_a を経由して行なわれます。つまり:

foo(1,*[2,3,4])
foo(1,*[])
foo(1,*[2,3,4],5)
foo(1,*[2,3,4],5,*[6])

は、それぞれ

foo(1,2,3,4)
foo(1)
foo(1,2,3,4,5)
foo(1,2,3,4,5,6)

と同じです。

 配列展開ね。Pythonにもあったやつ。

 ハッシュ(辞書、連想配列)でも同じことができる。なぜかこの公式ドキュメントにまったく説明がないけど。展開するときは**を頭につける。メソッドの引数にもできる。ようするにPythonと同じ。

def m(*args, **kwargs)
  p args   #=> [1, 2, 3]
  p kwargs #=> {:key=>"value", :k2=>"v2"}
end
m *[1,2,3], **{key:'value', k2:'v2'}

最後の引数の直前に & がついている場合、その引数で指定した手続きオブジェクト(Proc)やメソッドオブジェクト(Method)がブロックとしてメソッドに渡されます。詳細は ブロック付きメソッド呼び出し を参照してください。

 コードで書くと以下。

def m
  yield
end
m { p 'ブロック付きメソッド呼び出し1' }
def m(&block)
  block.call()
end
m { p 'ブロック付きメソッド呼び出し2' }

メソッド呼び出しの際、private なメソッドは関数形式(レシーバを省略した形式)でしか呼び出すことができません。また protected なメソッドはそのメソッドを持つオブジェクトのメソッド定義式内でなければ呼び出せません。クラス/メソッドの定義/呼び出し制限 を参照して下さい。

 privateメソッド呼出に関しては困ることはないと思う。もし親クラスのprotectedpublicなメソッドと名前重複したら、親クラスのそれらにsuperを付与して呼び出せばよいのだと思う。

 あとデザインパターン的に、継承よりも集約するほうが疎結合になって良いとされる。なのでprotectedに頼るような実装は避けたほうがよいかもしれない。

ハッシュ式の解説(リテラル/ハッシュ式)にも書かれている通り、メソッド呼出の末尾にHashを渡す場合は {, } を省略することができます。これを用いることでキーワード引数を渡すことができます。この Hash はクラス/メソッドの定義/メソッド定義で解説されているキーワード引数に分解されて引き渡されます。

 ハッシュをメソッド引数として渡すとキーワード引数になる。それはいいのだが、{,}の省略って嬉しいか? =>を使うと逆に長くなってしまうこともあるんだが。ふつうに名前:値のキーワード引数形式のほうがいいと思う。

def m(a:, b:, c:)
  p "#{a} #{b} #{c}"
end
m **{a:'A', b:'B', c:'C'}
#m a=>'A', b=>'B', c=>'C'       #=> undefined local variable or method `a' for main:Object (NameError)
#m :a=>'A', :b=>'B', :c=>'C'    # 正常だが冗長。 => を使う意味がない
#m 'a'=>'A', 'b'=>'B', 'c'=>'C' #=> missing keywords: :a, :b, :c (ArgumentError)
#m a='A', b='B', c='C'          #=> wrong number of arguments (given 3, expected 0; required keywords: a, b, c) (ArgumentError)
m a:'A', b:'B', c:'C'

メソッド呼び出しで .' の代わりに&.' を使うことができます。この形式でメソッドを呼びだそうとすると、レシーバが nil の場合は以下のように働きます。

  • 引数の評価が行なわれない
  • メソッド呼び出しが行われない
  • nil を返す
  • レシーバが nil でない場合は通常のメソッド呼び出しが行われます。
foo = 13
foo&.to_s # => "13"
foo = nil
foo&.to_s # nil, not ""

`&.' は要素代入(アトリビュート)に対しても使えます。

foo&.bar = "abc" # for `bar=' method

 つまりNULL参照エラーを回避できるってことか。これは便利。

 レシーバがnilになりうるときだけ&.参照すれば実行せずに通過してくれる。

super

例:

super
super(1,2,3)

文法:

super
super(式, ... )

super は現在のメソッドがオーバーライドしているメソッドを呼び出します。括弧と引数が省略された場合には現在のメソッドの引数がそのまま引き渡されます。引数を渡さずにオーバーライドしたメソッドを呼び出すには super() と括弧を明示します。

例:

class Foo
  def foo(arg=nil)
    p arg
  end
end

class Bar < Foo
  def foo(arg)
    super(5)       # 5 を引数にして呼び出す
    super(arg)     # 5 を引数にして呼び出す
    super          # 5 を引数にして呼び出す super(arg) の略記法
    arg = 1
    super          # 1 を引数にして呼び出す super(arg) の略記法
    super()        # 引数なしで呼び出す
  end
end
Bar.new.foo 5

 え、superって親インスタンスでなく親メソッドなの?! 他言語と違う。

 そういえばRubyは継承だけでなくMixinできるからなぁ。superが何を指しているのか一意に特定できるのはオーバーライドしたメソッドだけってことか。親クラスはMixinされちゃったらどれがsuperか一意に特定できない。

 Rubyはたしかancestorsで継承やMixinしたクラスのリストを取得できる。その順に指定したメソッドがあればそれを返す。なのでsuperの指定は他言語とちがう。

class D
  def m(msg)
    p "D: #{msg}"
  end
end
class E < D
  def m(msg)
    print 'E: '
    super(msg)
  end
end
E.new.m '引数' #=> E: D: 引数

 継承やMixinした他のメソッドはselfやその省略で呼べるはず。ただ、同名のオーバーライド元だけはsuperで呼び出す。それ以外に指名する方法がないから。

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

例:

[1,2,3].each do |i| print i*2, "\n" end
[1,2,3].each {|i| print i*2, "\n" }

文法:

method(arg1, arg2, ...)  do [`|' 式 ... `|'] 式 ... end
method(arg1, arg2, ...) `{' [`|' 式 ... `|'] 式 ... `}'
method(arg1, arg2, ..., `&' proc_object)

ブロック付きメソッドとは制御構造の抽象化のために用いられるメソッドです。最初はループの抽象化のために用いられていたため、特にイテレータと呼ばれることもあります。 do ... end または { ... } で囲まれたコードの断片 (ブロックと呼ばれる)を後ろに付けてメソッドを呼び出すと、そのメソッドの内部からブロックを評価できます。ブロック付きメソッドを自分で定義するには yield 式を使います。

 やっと説明きたよ。ブロック付きメソッドイテレータってことね。ほかの言語からしてみると全然違うんだけど。クロージャのことだと思うけど。

{ ... } の方が do ... end ブロックよりも強く結合します。次に例を挙げますが、このような違いが影響するコードは読み辛いので避けましょう:

foobar a, b do .. end   # foobarの引数はa, bの値とブロック
foobar a, b { .. }      # ブロックはメソッドbの引数、aの値とbの返り値とがfoobarの引数

 ん? 違いが影響するコード?

 そもそもdo..end{}の違いって、1行で書けるかどうかの違いがあるよね? それを先に説明してほしいんだが。と思ったら{}でも複数行で書けた。あれ?

def m
  yield
end
m {p "call"}
m {
  p "call"
}
m do p "call"; end
m do
  p "call"
end

 まあいいや。読み進めよう。

ブロックの中で初めて代入された(宣言された)ローカル変数はそのブロックの中でだけ有効です。例えば:

foobar {
  i = 20                # ローカル変数 `i' が宣言された
   ...
}
print defined? i        # `i' はここでは未定義なので false
foobar a, b do
  i = 11                # まったく別の変数 i の宣言
   ...
end

以下は逆にブロック外でも有効な例です。

i = 10
[1,2,3].each do |m|
  p i * m               # いきなり i を使える
end

 スコープが違うってことね。

ブロック 外側の変数を参照
{} できない
do..end できる

 その話は知ってた。

ブロックの部分だけを先に定義して変数に保存しておき、後からブロック付きメソッドに渡すことも出来ます。それを実現するのが手続きオブジェクト(Proc)です。それをブロックとして渡すにはブロック付きメソッドの最後の引数として `&' で修飾した手続きオブジェクトを渡します。Proc の代わりにメソッドオブジェクト(Method)を渡すことも出来ます。この場合、そのメソッドを呼ぶ手続きオブジェクトが生成され渡されます。

# 1引数の手続き(その働きは引数をpで印字すること)を生成し、変数pobjに格納
pobj = proc {|v|
  p v
}

[1,2,3].each(&pobj) # 手続きオブジェクトをブロックの代わりに渡している
=> 1
   2
   3

 クロージャを変数にするならprocを使え、ということか。

to_proc メソッドを持つオブジェクトならば、`&' 修飾した引数として渡すことができます。デフォルトで Proc、Method オブジェクトは共に to_proc メソッドを持ちます。to_proc はメソッド呼び出し時に実行され、Proc オブジェクトを返すことが期待されます。

class Foo
  def to_proc
    Proc.new {|v| p v}
  end
end

[1,2,3].each(&Foo.new)

=> 1
   2
   3

 なんであれprocを返すならメソッド経由でもOKと。

ブロック付きメソッドの戻り値は、通常のメソッドと同様ですが、ブロックの中から 制御構造/break により中断された場合は nil を返します。

 はい。

break に引数を指定した場合はその値がブロック付きメソッドの戻り値になります。

 はい。

yield

自分で定義したブロック付きメソッドでブロックを呼び出すときに使います。 yield に渡された値はブロック記法において | と | の間にはさまれた変数(ブロックパラメータ)に代入されます。

 yieldはブロック呼出式である。あきらかに他の言語のイテレータ、ジェネレータと違う。

例:

yield data

文法:

yield `(' [式 [`,' 式 ... ]] `)'
yield [式 [`,' 式 ... ]]

引数をブロックパラメータとして渡してブロックを評価します。yield はイテレータを定義するために クラス/メソッドの定義/メソッド定義 内で使用します。

# ブロック付きメソッドの定義、
# その働きは与えられたブロック(手続き)に引数1, 2を渡して実行すること
def foo
  yield(1,2)
end

# fooに「2引数手続き、その働きは引数を配列に括ってpで印字する」というものを渡して実行させる
foo {|a,b|
  p [a, b]
}  # => [1, 2] (要するに p [1, 2] を実行した)

# 今度は「2引数手続き、その働きは足し算をしてpで印字する」というものを渡して実行させる
foo {|a, b|
  p a + b
}  # => 3 (要するに p 1 + 2 を実行した)

# 今度のブロック付きメソッドの働きは、
# 与えられたブロックに引数10を渡して起動し、続けざまに引数20を渡して起動し、
# さらに引数30を渡して起動すること
def bar
  yield 10
  yield 20
  yield 30
end

# barに「1引数手続き、その働きは引数に3を足してpで印字する」というものを渡して実行させる
bar {|v|
  p v + 3
}
# => 13
#    23
#    33 (同じブロックが3つのyieldで3回起動された。
#        具体的には p 10 + 3; p 20 + 3; p 30 + 3 を実行した)

# Array#eachの(粗製乱造の)類似品
def iich(arr) # 引数に配列を取る
  idx = 0
  while idx < arr.size
    yield(arr[idx]) # 引数の各要素毎に、その要素を引数にしてブロックを起動
    idx += 1
  end
end

sum = 0
iich([1, 4, 9, 16, 25]) {|elem|
  sum += elem
}
p sum # => 55

ブロックパラメータの代入は演算子式/多重代入と同じルールで行われます。また yield を実行したメソッドにブロックが渡されていない (ブロック付きメソッド呼び出しではない)時は例外 LocalJumpError が発生します。

 記法は|p1,p2,...|と特殊。

yield はブロック内で最後に評価した式の値を返します。また、 制御構造/next によりブロックの実行が中断された場合は nil を返します。

 はい。

next に引数を指定した場合はその値が yield の戻り値になります。

 はい。

ブロックパラメータの挙動

メソッド呼び出しと挙動が異なります。 lambda でないブロックを呼び出したとき

  • 引数の数が違ってもエラーになりません。
  • 配列をひとつ渡したときにそれが引数の並びとして展開されることがあります。

 lambdaの説明って今まで一度も出てこなかったよね? ここで急に出てきたよ。ちゃんと説明してよ。

def foo
  yield 1,2,3
end

foo{|v| p v}       #=> 1

def bar
  yield [1,2,3]
end

bar{|a, b, c| p a} #=> 1

def hoge
  yield [1,2,3],4,5
end

hoge{|a, b, c| p a} #=> [1,2,3]

http://www.a-k-r.org/d/2007-08.html#a2007_08_16_1

.() および ::() 形式のメソッド呼び出し(callメソッドの糖衣構文) 下記はcallメソッドの糖衣構文です。 Proc#callにも言及がありますが、Proc以外のオブジェクトに対しても(callメソッドさえ定義されていれば)使えます。

例:

foo.(100)      # foo.call(100)と全く同じ意味
foo::(100)     # foo.call(100)と全く同じ意味
foo.(100)      # foo.call(100)と全く同じ意味
foo::(100)     # foo.call(100)と全く同じ意味

文法:

`.' `(' [[`*'] 式] ... [`&' 式] `)'
式 `::' `(' [[`*'] 式] ... [`&' 式] `)'

 これはスタイルガイドで非推奨だった記法。call()を使ったほうが良さそう。

所感

 superが以外だった。

対象環境

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