やってみる

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

「クラス/メソッドの定義」を読む1

 内部クラスも定義できる。新たに知ったのはそのくらい。

成果物

情報源

クラス/メソッドの定義

  • クラス/メソッドの定義:
    • クラス定義
    • 特異クラス定義
    • モジュール定義
    • メソッド定義
    • 演算子式の定義
    • メソッド定義のネスト
    • メソッドの評価
    • 特異メソッド定義
    • クラスメソッドの定義
    • 呼び出し制限
  • 定義に関する操作:
    • alias
    • undef
    • defined?

クラス定義

例:

class Foo < Super
  def test
     :
  end
     :
end

文法:

class 識別子 [`<' superclass ]
  式..
end

文法:

class 識別子 [`<' superclass ]
  式..
[rescue [error_type,..] [=> evar] [then]
  式..]..
[else
  式..]
[ensure
  式..]
end

クラスを定義します。クラス名はアルファベットの大文字で始まる識別子です。

 頭文字は大文字でないとエラーになる。

class a; end #=> class/module name must be CONSTANT

rescue/ensure 節を指定し、例外処理ができます。例外処理については制御構造/begin参照。

 クラス定義で例外処理したがる動機があるのか? わからん。

クラス定義は、識別子で指定した定数へのクラスの代入になります (Ruby では、クラスもオブジェクトの一つで Classクラスのインスタンスです)。

 クラスは定数の一種。なので定数と同じく頭文字が大文字。

クラスが既に定義されているとき、さらに同じクラス名でクラス定義を書くとクラスの定義の追加になります。ただし、元のクラスと異なるスーパークラスを指定すると TypeError が発生します。

class Foo < Array
  def foo
  end
end

# 定義を追加(スーパークラス Array を明示的に指定しても同じ)
class Foo
  def bar
  end
end

# 間違ったスーパークラスを指定するとエラー
class Foo < String
end
# => superclass mismatch for class Foo (TypeError)

 クラスは再定義できてしまう。このせいでバグがどこにあるのか探すのに全コードファイルを見なければならない。カプセル化の意義が失われてしまうほどの残念ぶり。

クラス定義式の中は self がそのクラスであることと、 呼び出し制限のデフォルトが異なること以外にトップレベルとの違いはありません。クラス定義式中には任意の式を書くことができクラス定義の際に実行されます。

 でもインスタンスメソッド定義内におけるselfはクラスではなくインスタンスを指す。紛らわしい。

クラス定義はネスト(入れ子)にして定義できます。以下の例で入れ子の外側のクラス Foo と内側のクラス Bar の間には、定数 Bar が Foo の中の定数 Foo::Bar であること以外、継承関係などの機能的な関連はまったくありません。

class Foo
  class Bar
  end
end

クラス Foo が既に定義されていれば、以下の書き方もできます。

class Foo
end

class Foo::Bar
end

 いわゆる内部クラスというやつか。privateにできるのかな? 試してみたがエラーにならなかったのでprivateにはできないっぽい。

class C
  private
  class D; end
end
C.new
C::D.new

 念のためググったらprivate_constantというのでできるっぽい。

class E
  class F; end
  private_constant :F
end
E.new
E::F.new #=> private constant E::F referenced (NameError)

 相変わらず情報がまとまっていない。またも公式ドキュメント外で新たな情報を発見した。こんなことばっかり。本当にこのドキュメントで構文を学べるのか? 甚だ疑問。

クラスのネストは、意味的に関連するクラスを外側のクラス/モジュールでひとまとまりにしたり、包含関係を表すために使用されます。

# 関連するクラスを Net というカテゴリにまとめる
# このような場合は外側は普通モジュールが利用される
# (Net のインスタンスがない。Net を include できるなどのため)
module Net
  class HTTP
  end
  class FTP
  end
end

obj = Net::HTTP.new

# あるいは

include Net
obj = HTTP.new

# 以下のような使い方は組み込みのクラスにも見られる
# 利用者は File::Constants を include することで、
# File::RDONLY などと書かずに直接 RDONLY と書くことができる。
class File
  module Constants
    RDONLY = 0
    WRONLY = 1
  end
  include Constants
end

File.open("foo", File::RDONLY)

# あるいは

include File::Constants
File.open("foo", RDONLY)

# 上記はあくまでも例である。実際の File.open ではより簡便な
# File.open("foo", "r") という形式が使われる

 ようするに内部クラスや内部モジュールは名前空間として使えるってことね。

クラス定義式は、最後に評価した式の結果を返します。最後に評価した式が値を返さない場合は nil を返します。

 クラス定義ですら式であり値を返す。はたしてそれに何の意味があるのかは知らないが。

特異クラス定義

例:

obj = Object.new # obj = nil でも可
class << obj
  def test
     :
  end
     :
end

文法:

class `<<' expr
  式..
end

文法:

class `<<' expr
  式..
[rescue [error_type,..] [=> evar] [then]
  式..]..
[else
  式..]
[ensure
  式..]
end

クラス定義と同じ構文で特定のオブジェクトにメソッドやインスタンス変数を定義/追加します。この構文の内部で定義したメソッドや定数は指定したオブジェクトに対してだけ有効になります。 Object#clone で生成したオブジェクトには引き継がれますが, Object#dup で生成したオブジェクトには引き継がれません.

 ややこしいな。cloneだのdupだの知らんよ。

rescue/ensure 節を指定し、例外処理ができます。例外処理については制御構造/begin参照。

 定義の時点で例外処理したがる理由がわからない。

特異クラス定義式は、最後に評価した式の結果を返します。最後に評価した式が値を返さない場合は nil を返します。

 やはりこれも式であり値を返すようだ。その意義はわからない。

 できる、というだけでなくベストプラクティスを絡めて話してほしい。

モジュール定義

例:

module Foo
  def test
     :
  end
     :
end

文法:

module 識別子
  式..
end

文法:

module 識別子
  式..
[rescue [error_type,..] [=> evar] [then]
  式..]..
[else
  式..]
[ensure
  式..]
end

モジュールを定義します。モジュール名はアルファベットの大文字で始まる識別子です。

 クラスと同じ。

rescue/ensure 節を指定し、例外処理ができます。例外処理については制御構造/begin参照。

 クラスと同じ。

モジュール定義は、識別子で指定した定数へのモジュールの代入になります。 Ruby では、モジュールもオブジェクトの一つで Module クラスのインスタンスです。モジュールが既に定義されいるとき、さらに同じモジュール名でモジュール定義を書くとモジュールの定義の追加になります。

 クラスと同じ。

モジュール定義式は、最後に評価した式の結果を返します。最後に評価した式が値を返さない場合は nil を返します。

 クラスと同じ。

 これではクラスとの違いがわからない。私が覚えている違いをメモっておく。

  • moduleclassと違ってnewできない
  • moduleclassinclude,prepend,extendされる側
  • moduleはモジュール関数の定義ができる(スタティック関数のようなもの)

メソッド定義

例:

def fact(n)
  if n == 1 then
    1
  else
    n * fact(n-1)
  end
end

文法:

def メソッド名 ['(' [arg0 ['=' default0]] ... [',' '*' rest_args [, post ...]] [',' key1: [val1]] ... [',' '**'kwrest] [',' '&' block_arg]`)']
  式.. (body)
[rescue [error_type,..] [=> evar] [then]
  式..]..
[else
  式..]
[ensure
  式..]
end

この定義のある場所にメソッドを定義します。すなわち、クラス/モジュール定義中ならばそのクラス/モジュールのメソッドを定義します。トップレベルならばどこからでも呼べるメソッドを定義します。このようなメソッドは結果として他の言語における「関数」のように使えます。

例:

def hello    # 引数のないメソッド。
  puts "Hello, world!"
end

def foo(a, b)    # 引数のあるメソッド。括弧を省いてdef foo a, bとも
  a + 3 * b
end

 はい、知ってます。

メソッド名としては通常の識別子の他に、再定義可能な演算子(例: ==, +, - など 演算子式 を参照)も指定できます(演算子式の定義参照)。

例:

class Vector2D
  attr_accessor :x, :y   # インスタンス変数@x, @yに対応するゲッタとセッタを定義
  def initialize(x, y)   # コンストラクタ
    @x = x; @y = y   # @がつくのがインスタンス変数(メンバ変数)
  end
  def ==(other_vec)   # いわゆる演算子オーバーライド
    other_vec.x == @x && other_vec.y == @y
  end
  def +(other_vec)
    Vector2D.new(other_vec.x + @x, other_vec.y + @y)
  end
  ...
end
vec0 = Vector2D.new(10, 20); vec1 = Vector2D.new(20, 30)
p vec0 + vec1 == Vector2D.new(30, 50) #=> true

 知ってます。他の言語でいうところの演算子オーバーライド。

仮引数にデフォルト式が与えられた場合、メソッド呼び出しで実引数を省略したときのデフォルト値になります。ただし実引数との対応を取るため、i番目の引数にデフォルト値を指定したならば、 i+1番目以降でも全てデフォルト値を指定するか、可変長引数を利用しなければなりません(詳細は後述)。デフォルト式の評価は呼び出し時にメソッド定義内のコンテキストで行われます。

例:

def foo(x, y = 1)    # 2番目の引数yにデフォルト値を指定
  10 * x + y
end
p foo(1, 5)  #=> 15
p foo(3)     #=> 31
p foo        #=> ArgumentError (wrong number of arguments)

$gvar = 3
def bar(x, y = $gvar)  # 確かに定義時には$gvar == 3だが
  10 * x + y
end
$gvar = 7
# 呼び出し時の$gvarの値が使われる
p bar(5)   #=> 57 (!= 53)

 ふつうデフォルト値には定数を使うと思うのだが。たぶん説明のためだろう。

 ほかのコンパイルプログラミング言語では変数をデフォルト値にはできない。そのへんの違いを明記したのだろう。

仮引数の直前に * がある場合には残りの実引数 (後述の post 引数を除く) はみな配列とし てこの引数に格納されます。可変長引数、rest 引数などと呼ばれる機能です。このような引数は 1 つしか作れません。

例:

def foo(x, *xs)
  puts "#{x} : #{xs.inspect}"   # Object#inspect は p のような詳細な内部表示
end
foo(1)        #=> 1 : []
foo(1, 2)     #=> 1 : [2]
foo(1, 2, 3)  #=> 1 : [2, 3]

def bar(x, *) # 残りの引数を単に無視したいとき
  puts "#{x}"
end
bar(1)        #=> 1
bar(1, 2)     #=> 1
bar(1, 2, 3)  #=> 1

 C言語にもあった可変長引数ね。

Ruby 1.9 以降では可変長引数よりも後にまだ通常の引数を置くことができます。

 それは混乱しそう。たしかに以下コードでエラーにならなかった。

def m(*args, p1, p2); end

 どうやって渡せばいいんだ?

def m(*args, p1, p2)
  p args
  p p1
  p p2
end
m 1,2,3     #=> [1] 2 3
m 1,2,3,4,5 #=> [1,2,3] 4 5
m 1 #=>  wrong number of arguments (given 1, expected 2+) (ArgumentError)

 ああ、後ろの数から逆算しているのかな?

 じゃあ前後に位置引数があったときは?

def m(p1, p2, *args, p3, p4)
  p p1
  p p2
  p args
  p p3
  p p4
end
m 1,2,3,4,5     #=> 1 2 [3] 4 5
m 1,2,3,4,5,6 #=> 1 2 [3,4] 5 6
m 1,2,3,4 #=> 1 2 [] 3 4

 なるほど。位置引数の数と位置の分だけ引数があれば、あとは可変長引数ができるかぎり調整する感じか。

最後の仮引数の直前に & があるとこのメソッドに与えられているブロックが手続きオブジェクト(Proc)としてこの引数に格納されます。これは、イテレータを定義する方法の一つです。イテレータを定義する代表的な方法は yield を呼び出すことです。他に Proc.new/Kernel.#proc を使う方法などもあります。ブロックが与えられなかった場合のブロック引数の値はnilです。

例:

def foo(cnt, &block_arg)
  cnt.times { block_arg.call } # ブロックに収まったProcオブジェクトはcallで実行
end
foo(3) { print "Ruby! " } #=> Ruby! Ruby! Ruby!

 はい、知ってます。他の言語からみると奇妙な書き方。

メソッド定義において、仮引数はその種類毎に以下の順序でしか指定することはできません。いずれも省略することは可能です。

  1. デフォルト式のない引数(複数指定可)
  2. デフォルト式のある引数(複数指定可)
  3. *を伴う引数(1つだけ指定可)
  4. デフォルト式のない引数(複数指定可)
  5. キーワード引数(複数指定可)
  6. **を伴う引数(1つだけ指定可)
  7. &を伴う引数(1つだけ指定可)

 これは明記してくれて嬉しい。超大事。

 デフォルト式のある引数と、キーワード引数って紛らわしい。

名前 コード
デフォルト式のある引数 p1 = 'v'
キーワード引数 p1: 'v', p1:

例:

# すべて持つ(極端な例なのでおすすめしない)
def f(a, b, c, m = 1, n = 1, *rest, x, y, z, k: 1, **kwrest, &blk)
  puts "a: %p" % a
  puts "b: %p" % b
  puts "c: %p" % c
  puts "m: %p" % m
  puts "n: %p" % n
  puts "rest: %p" % rest
  puts "x: %p" % x
  puts "y: %p" % y
  puts "z: %p" % z
  puts "k: %p" % k
  puts "kwrest: %p" % kwrest
  puts "blk: %p" % blk
end

f("a", "b", "c", 2, 3, "foo", "bar", "baz", "x", "y", "z", k: 42, u: "unknown") { }
  #=> a: "a"
      b: "b"
      c: "c"
      m: 2
      n: 3
      rest: "foo"
      x: "x"
      y: "y"
      z: "z"
      k: 42
      kwrest: {:u=>"unknown"}
      blk: #<Proc:0x007f7e7d8dd6c0@-:16>

例: イテレータの定義

# yield を使う
def foo
  # block_given? は、メソッドがブロックを渡されて
  # 呼ばれたかどうかを判定する組み込み関数
  if block_given?
    yield(1,2)
  end
end

# Proc.new を使う
def bar
  if block_given?
    Proc.new.call(1,2)    # proc.call(1,2) でも同じ(proc は組み込み関数)
  end
end

# 応用: 引数として Proc オブジェクトとブロックの
# 両方を受け付けるイテレータを定義する例
def foo(block = Proc.new)
  block.call(1,2)
end
foo(proc {|a,b| p [a,b]})
foo {|a,b| p [a,b]}

# ブロック引数を使う 
def baz(&block)
  if block
    block.call(1,2)
  end
end

 というかif block&.を使えば省略できるはずでは? &.で参照すると左辺レシーバがnilのときは実行しないんだったよね?

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

  • 引数の評価が行なわれない
  • メソッド呼び出しが行われない
  • nil を返す
  • レシーバが nil でない場合は通常のメソッド呼び出しが行われます。

 というわけで書いてみる。ついでに呼出の()も省略。

def m(&block)
  block&.call 1, 2
end
m {|a,b| p '#{a} #{b}'}
m
#m proc {|a,b| p '#{a} #{b}'} #=> wrong number of arguments (given 1, expected 0) (ArgumentError)

 でもprocは受け付けなかった。デフォルト値ありで省略できるようにしないとダメらしい。さらにブロック引数&にしてはダメっぽい。めんどくせぇ……。

def m(block = proc {})
  block&.call 1, 2
end
m
m {|a,b| p '#{a} #{b}'}
m proc {|a,b| p '#{a} #{b}'}

 できた。ブロック{}do..end, proc, lambdaクロージャとして統一してほしい。定義の記法も違えば仮引数の記法まで違う。ほぼ同じことを指すのに記法が違うのは紛らわしいだけ。これは書いてて楽しくないコードだ。

 あとさ、前から思っていたけどブロック引数ってひとつしか作れないよね? ヒアドキュメントみたく複数書けたらよかったのに。

p <<FIRST, <<SECOND
最初
FIRST
二番目
SECOND

 あ、でもメソッド定義には書けないみたい。

def m(<<~FIRST) #=> syntax error, unexpected string literal, expecting ')'
  最初
FIRST
end

 仮に複数のブロックを追加できるメソッド定義記法があっても意味はなさそう。

def m(p1, p2, <<INI, <<FIN)
  p '開始処理'
INI
  p '終了処理'
FIN
  INI.call
  p '中心処理'
  FIN.call
end

 やるなら呼出だけど、あまり美しくない。

def m(p1, p2, INI=nil, FIN=nil)
  INI&.call
  p '中心処理'
  FIN&.call
end
m 1, 2 { p '開始処理' } { p '終了処理' }

 現状でもproc変数に代入すれば複数のproc呼出を書ける。でもブロックはダメ。

def m(p1, p2, ini=nil, fin=nil)
  ini&.call
  p "#{p1} #{p1}"
  fin&.call
end
m 1, 2
m 1, 2, proc{p '開始'}, proc{p '終了'}
m(1, 2) {p '開始'} # ブロックは実行されない
#m 1, 2 {p '開始'} #=> syntax error, unexpected '{', expecting end-of-input
#m 1, 2 {p '開始'} {p '終了'} #=> syntax error, unexpected '{', expecting end-of-input
#m 1, 2, {p '開始'}, {p '終了'} #=> syntax error, unexpected string literal, expecting `do' or '{' or '('

 ブロック引数はひとつだけしか定義できない。

def m(p1, p2, &ini=nil, &fin=nil) #=> syntax error, unexpected '=', expecting ')'
  ini&.call
  p "#{p1} #{p1}"
  fin&.call
end

 処理を引数にしたいときは以下のようになる。

  • 処理引数がひとつである
    • ブロック{}である
      • ブロック引数省略
      • ブロック引数明記
    • proc{}である
      • デフォルト式なし(必須)
      • デフォルト式あり(任意)
        • 引数定義: f = nil
        • 呼出: f&.call
    • ブロックとprocの両方受け付ける
      • 引数定義: f = nil
      • 呼出: f&.call
  • 処理引数がふたつ以上ある
    • proc{}である
      • デフォルト式なし(必須)
      • デフォルト式あり(任意)
        • 引数定義: f = nil
        • 呼出: f&.call

 ムダに記法が多い。たぶんRubyの構文を改善しつづけてきて、徐々に面倒くさくなってきたのだろう。単にクロージャの扱いとして統一してほしいのが本音。ほかのプログラミング言語とくらべて面倒くさすぎる。

またメソッド実行時の例外を捕捉するために begin 式と同様のrescue, else, ensure 節を指定できます。例外処理については制御構造/begin参照。

 メソッド「定義時」ではなく「実行時」と書いてある。これなら使うことはよくあるだろう。

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

 それを使いたがるときはあるのだろうか?

m = def m; end

@see https://magazine.rubyist.net/articles/0041/0041-200Special-kwarg.html