Ruby 3.2.0 Preview1 が出た

Ruby 3.2.0 Preview1 が出ました。
RUby 3.1.0 Preview1 が去年の11月に出たことを考えると Ruby 3.2 の Preview1 は出るのが早かったですね?

Ruby 3.2.0 Preview1 の新機能としては WASIベースのWebAssemblyサポートRegexp timeout になりますね。
気になる人は試してみるとよいと思います。

余談1: git protocol が廃止された

rbenv で Ruby 3.2.0 Preview1 をインストールするために ruby-build を更新しようとするとエラーが出た。

$ git pull
fatal: リモートエラー:
  The unauthenticated git protocol on port 9418 is no longer supported.
Please see https://github.blog/2021-09-01-improving-git-protocol-security-github/ for more information.

これは暗号化されてない git protocol が廃止されたためらしい。
雑に remote 先を変えて対応。

$ git remote -v
origin  git://github.com/sstephenson/ruby-build.git (fetch)
origin  git://github.com/sstephenson/ruby-build.git (push)
$ git remote set-url origin https://github.com/rbenv/ruby-build.git
$ git remote -v
origin  https://github.com/rbenv/ruby-build.git (fetch)
origin  https://github.com/rbenv/ruby-build.git (push)

余談2:Ruby 3.2.0 から libyaml が同梱されなくなった

数日前から rbenv install 3.2.0-dev すると require': cannot load such file -- digest (LoadError) になってビルドに失敗していました。
これ、結構困っていたんですが原因は Ruby 3.2.0 から libyaml が同梱されなくなったからでした。
なので sudo apt install libyaml-dev する事で解決しました。
これは Ruby 3.2.0 Preview1 のリリースノートに書いてあったから気づけたんですが、読まなかった一生わからなかったなあ。
同様に Ruby 3.2.0 Preview2 から libffi が同梱されなくなるらしいので注意する必要があります。

Ruby で新しい Unicode 規格にバージョンアップする時になにを行っているのかまとめてみた

Ruby は定期的に Unicode の新しい規格に対応しているんですがその時になにが行われているのかが気になったので調べてみました。
この記事では『Ruby の Unicode 14.0.0 対応がなにを行っているか』を追っていきたいと思います。
ちなみに Unicode 14.0.0 は次の Ruby 3.2 でサポートされる予定です。

RubyUnicode 14.0.0 対応

RubyUnicode 14.0.0 対応のチケットは以下になります。

上のチケットの作業コミットはコメントに書かれているんですが以下の通りです。

commit 48f1e8c5d85043e6adb8e93c94532daa201d42e9
Author: Martin Dürst <duerst@it.aoyama.ac.jp>
Date:   Tue Mar 15 18:55:16 2022 +0900

    Fix version check to use Emoji version for emoji-variation-sequences.txt

commit 56d9d78f14b73cb9f609558e6b760dde50872fb6
Author: Martin Dürst <duerst@it.aoyama.ac.jp>
Date:   Tue Mar 15 17:17:15 2022 +0900

    Remove Unicode 13.0.0 related files

commit 267f0089d3255c1f06ab5adf9f6c77b1ccfd2771
Author: Martin Dürst <duerst@it.aoyama.ac.jp>
Date:   Mon Mar 14 08:37:53 2022 +0900

    clarify meaning of version guards for Unicode version specs [ci skip]

commit 1b571d0abf6070673320b11a30769bbe74d12e39
Author: Benoit Daloze <eregontp@gmail.com>
Date:   Sun Mar 13 13:18:56 2022 +0100

    Fix guards for unicode versions specs

commit 45187a0fcddecc74dacc1881f2405a5ebe198081
Author: Martin Dürst <duerst@it.aoyama.ac.jp>
Date:   Sun Mar 13 10:52:24 2022 +0900

    comment out failing Unicode/Emoji version checks temporarily

commit 8f59482f5d1f77542fe3c8e8434cb1dbbc764ecf
Author: Martin Dürst <duerst@it.aoyama.ac.jp>
Date:   Sat Mar 12 21:33:51 2022 +0900

    add some tests for Unicode Version 14.0.0

commit 9b545b0caf2ccc89718ba02ff631d2a68b96a831
Author: Martin Dürst <duerst@it.aoyama.ac.jp>
Date:   Fri Mar 11 17:18:42 2022 +0900

    update specs to check for Unicode Version 14.0.0/Emoji Version 14.0

commit 2672502457523317268ac24704cf85df91e2cae6
Author: Martin Dürst <duerst@it.aoyama.ac.jp>
Date:   Fri Mar 11 17:11:32 2022 +0900

    mention Unicode Version 14.0.0

commit 8e1f3a96aecb3defc34556d75e3d2a0867416082
Author: Martin Dürst <duerst@it.aoyama.ac.jp>
Date:   Fri Mar 11 16:48:26 2022 +0900

    switch UNICODE_BETA back to NO

commit 45e0711f29f9ce65cd34ad14e3af1546ccc7252e
Author: Martin Dürst <duerst@it.aoyama.ac.jp>
Date:   Thu Dec 9 16:41:09 2021 +0900

    update Unicode Version to 14.0.0 and Emoji version to 14.0

と、いうことでまずはこのコミットの内容を下から時系列に1つ1つ見ていこうかな、と思います。

[45e0711f29] update Unicode Version to 14.0.0 and Emoji version to 14.0

common.mkUNICODE_VERSIONUNICODE_EMOJI_VERSION のバージョンが 14 に更新されています。
UNICODE_EMOJI_VERSION が別にあるんですね?
また作業当時はまだ beta 版だったので UNICODE_BETA = YES となっているぽい?
他には enc/unicode/14.0.0/casefold.henc/unicode/14.0.0/name2ctype.h の2つのファイルが新しく追加されて lib/unicode_normalize/tables.rb が更新されています。
このあたりのファイルはあとで詳しくみていきましょうかね。

[8e1f3a96ae] switch UNICODE_BETA back to NO

common.mkUNICODE_BETANO になっています。

[2672502457] mention Unicode Version 14.0.0

NEWS.md にドキュメントを追加しています。

[9b545b0caf] update specs to check for Unicode Version 14.0.0/Emoji Version 14.0

RbConfig::CONFIG['UNICODE_VERSION']RbConfig::CONFIG['UNICODE_EMOJI_VERSION'] に対するテストが追加されています。
これでバージョンを取得できるんですね、へー。

p RbConfig::CONFIG['UNICODE_VERSION']
# Ruby 3.1 => # "13.0.0"
# Ruby 3.2 => # "14.0.0"

p RbConfig::CONFIG['UNICODE_EMOJI_VERSION']
# Ruby 3.1 => # "13.1"
# Ruby 3.2 => # "14.0"

UNICODE_VERSIONUNICODE_EMOJI_VERSION で同じバージョンとは限らないんですね、へー。

[9b545b0caf] add some tests for Unicode Version 14.0.0

正規表現\p (Unicode プロパティによる文字クラス指定) のと String#upcase のテストが追加されています。

[45187a0fcd] comment out failing Unicode/Emoji version checks temporarily

テストをコメントアウトしています。何かあったんですかね?

[1b571d0abf] Fix guards for unicode versions specs

テストのコメントアウトを元に戻しています。

[267f0089d3] clarify meaning of version guards for Unicode version specs [ci skip]

テストのインデントを調整したりコメントを追加したりしています。

[56d9d78f14] Remove Unicode 13.0.0 related files

enc/unicode/13.0.0/casefold.henc/unicode/13.0.0/name2ctype.h を削除しています。
この2つはバージョンごとに新しいファイルを追加している感じですかね?

[48f1e8c5d8] Fix version check to use Emoji version for emoji-variation-sequences.txt

テストで参照しているバージョンを UNICODE_VERSION から EMOJI_VERSION に変更しています。
ただ、これは別のコミットで元に戻されているぽいですね。


と、いう感じでコミットを1つ1つみてみました。
実際の Unicode 対応としては、

  • enc/unicode/14.0.0/casefold.h
  • enc/unicode/14.0.0/name2ctype.h
  • lib/unicode_normalize/tables.rb

の3つのファイルが重要そうですね。
今度はこの3つのファイルがなにをやっているのか見ていきたいと思います。

enc/unicode/14.0.0/casefold.h

enc/unicode/14.0.0/casefold.h には Unicode の大文字、小文字の対応表が定義されています。
例えば Unicode 14.0.0 で追加された U+2C2F という文字も enc/unicode/14.0.0/casefold.h に新しく追加されています。

# Ruby 3.2 だとちゃんと小文字に変換される
p "\u2C2F".downcase.codepoints.map { _1.to_s(16) }
# Ruby 3.1 => ["2c2f"]
# Ruby 3.2 => ["2c5f"]

この enc/unicode/14.0.0/casefold.h ファイルは CaseFolding.txt を元に指定 enc/unicode/case-folding.rb機械的に生成されているみたいです。

enc/unicode/14.0.0/name2ctype.h

enc/unicode/14.0.0/name2ctype.h には正規表現POSIX 文字クラスの [:alpha:][:lower:]Unicode プロパティなどのメタデータが定義されています。
例えば Unicode 14.0.0 で追加された U+0870 という文字も enc/unicode/14.0.0/name2ctype.h に新しく追加されています。

p "\u0870" =~ /[[:alpha:]]/
# Ruby 3.1 => nil
# Ruby 3.2 => 0

この enc/unicode/14.0.0/name2ctype.h ファイルは tool/enc-unicode.rb から機械的に生成されているぽい?

lib/unicode_normalize/tables.rb

lib/unicode_normalize/tables.rbenc/unicode/14.0.0/casefold.henc/unicode/14.0.0/name2ctype.h とは違って新しくファイルが追加されているわけではなくて既存のファイルを更新しています。
lib/unicode_normalize/tables.rb には Unicode 正規化を行うためのメタデータが定義されています。
例えば NFKC で正規化した際の ① => 1 という変換テーブルもここで定義されています。

p "".unicode_normalize(:nfkc)
# => "1"

Unicode 14.0.0 で追加された U+A7F2 という文字も lib/unicode_normalize/tables.rb に追加されており、この文字を正規化すると "C" になります。

p "\uA7F2".unicode_normalize(:nfkc)
# Ryvt 3.1 => "\uA7F2"
# Ruby 3.2 => "C"

lib/unicode_normalize/tables.rb ファイルもまた template/unicode_norm_gen.tmpl を元にして機械的に生成されているぽいですかね?

まとめ

と、いう感じで簡単にですが RubyUnicode 14.0.0 に対応した時になにをしているのかを簡単にまとめてみました。
Ruby 側で対応している項目自体は多い(Unicode のアップデート内容による)んですが全体的に自動生成されるような仕組みがあるようなのですごい。
あと大文字小文字の変換テーブルや正規表現POSIX 文字クラス、Unicode 正規化のメタ情報も Ruby の処理系で保持しているんですね。
具体的にこのあたりのデータがどう使用されているのかあんまりわかっていないので次はこのあたりの実装がどうなっているのか調べてみるのとこあおもしろそうですねー。
次は Unicode 15.0.0 の対応もはじまると思うのでその時にまたどういうような対応を行っているのか追えるとよいですねー。

2022/03/31 今回の気になった bugs.ruby のチケット

今週は正規表現マッチするときのタイムアウトを導入するチケットがマージされました。

[Feature #17837] Add support for Regexp timeouts

  • 正規表現を比較する時にタイムアウトの概念を導入するチケット
  • 色々と議論があったが最終的にはタイムアウトの時間を制御する Regexp.timeout Regexp.timeout= が追加された
  • Regexp.timeoutタイムアウトする時刻(秒)を設定する事で『正規表現を比較する時に設定したタイムアウト秒を越えると例外が発生する』ようになる
    • デフォルトだと nil になっており nil だと例外は発生しない
# デフォルトだと nil
pp Regexp.timeout
# => nil

# この場合はタイムアウトしない
/A(B|C+)+D/ =~ "A" + "C" * 28 + "X"

# タイムアウトする時刻を設定する事ができる
Regexp.timeout = 0.1
pp Regexp.timeout
# => 0.1

# 正規表現の比較に 0.1秒以上かかると例外が発生する
begin
  /A(B|C+)+D/ =~ "A" + "C" * 28 + "X"
rescue => e
  pp e
  # => #<Regexp::TimeoutError: regexp match timeout>
end
  • また Regexp.newRegexp.compile もキーワード引数 timeout: を受け取るようになたt
# デフォルトだと timeout: nil
regexp = Regexp.new("A(B|C+)+D")
regexp =~ "A" + "C" * 28 + "X"

# 正規表現ごとに個別に timeout を設定することができる
regexp2 = Regexp.new("A(B|C+)+D", timeout: 0.5)
begin
  regexp2 =~ "A" + "C" * 28 + "X"
rescue => e
  pp e
  # => #<Regexp::TimeoutError: regexp match timeout>
end

[Feature #12655] Accessing the method visibility

  • メソッドの可視性の情報として『 [:public, :protected, :private, :undefined, :overridden] を取得したい』という要望のチケット
    • looksee という gem でそういう機能を実装している
    • 今はがんばって C拡張で実装しているが RubyAPI でもほしいらしい
  • :undefined の情報を取得する手段として Module#undefined_instance_methods が新しく追加される予定
class Super
  def hoge
  end
end

class Sub
  undef hoge
end

pp Sub.undefined_instance_methods
# => [:hoge]

[Feature #15357] Proc#parameters returns incomplete type information

  • 以下のようにブロックの引数で『オプショナル引数でないのに Proc#parameters:opt になる』のは正しくないんじゃないか、というバグ報告
# これは b がオプショナル引数
pr = proc { |a, b = 2| [a,b] }

# しかし Proc#parameters は a もオプショナル引数として情報を返す
p pr.parameters  # => [[:opt, :a], [:opt, :b]]

# 同様に以下のような場合も b もオプショナル引数として情報が返ってくる
pr = proc { |a = 1, b| [a,b] }
p pr.parameters  # => [[:opt, :a], [:opt, :b]]
  • これは proc の引数は全てオプショナル引数として定義されているためである
# オプショナル引数として定義していない a に引数を渡さなくてもエラーにはならない
pr = proc { |a, b = 2| [a,b] }
p pr.call
# => [nil, 2]

# こちらも同様
pr = proc { |a = 1, b| [a,b] }
p pr.call
# => [1, nil]
  • なので報告されたコード自体は意図する挙動になっている
  • ちなみに lambda の場合は必須引数として返ってくる
pr = lambda{|a, b=2| [a,b] }
p pr.parameters  # => [[:req, :a], [:opt, :b]]

pr = lambda{|a=1, b| [a,b] }
p pr.parameters  # => [[:opt, :a], [:req, :b]]
  • また、このように lambda であるようにパラメータ情報を受け取る手段として Proc#parameterslambda: false キーワード引数が追加された
    • これは既に Ruby 3.2 で対応済み
pr = proc{|a, b=2| [a,b] }
p pr.parameters                # => [[:opt, :a], [:opt, :b]]
p pr.parameters(lambda: true)  # => [[:req, :a], [:opt, :b]]

pr = proc{|a=1, b| [a,b] }
p pr.parameters                # => [[:opt, :a], [:opt, :b]]
p pr.parameters(lambda: true)  # => [[:opt, :a], [:req, :b]]

2022/03/24 今回の気になった bugs.ruby のチケット

今週は注釈付き代入演算子を提案するチケットなどがありました。

[Feature #18626] 注釈付き代入演算子 ()= の提案

  • 以下のような注釈付き代入演算子の提案になる
class Object
  # 代入するときの処理をフックする
  def self.()= (what)
    what.is_a? self or raise TypeRestrictionError
  end
end

# 代入する時に注釈を指定して定義する
age (Fixnum) = 30

# メソッドの引数にも定義できる
def add(a(Numeric), b(Numeric))
  a + b
end
add 1, "2" # raises TypeRestrictionError
  • この提案なんですが以下のような理由から Reject されています

文法的にも意味論的にも大変興味深い提案ですが、Ruby言語にいかなる形であれ型宣言や型注釈を導入するつもりはありません。すみません。

まつもと ゆきひろ /:|)

  • これは面白いなー既存の構文を壊さないようにしてなんか定義できないだろうか

[Feature #18644] Coerce anything callable to a Proc

  • 呼び出し可能オブジェクトを拡張するためのメソッドを追加する提案
  • #call メソッドを Proc 化する Object#to_proc
class Object
  # call メソッドを `Proc` オブジェクト化する
  def to_proc
    return method(:call).to_proc if respond_to?(:call)

    raise "Needs to respond to :call"
  end
end

class Proc
  def to_proc
    self
  end
end

callable.to_proc.curry[value]
  • 引数を Proc オブジェクト化する Kernel#Proc
class Kernel
  def Proc(value)
    if value.is_a?(::Proc)
      value
    elsif value.respond_to?(:call)
      value.method(:call).to_proc
    else
      raise "Needs to implement :call"
    end
  end
end

Proc(callable).curry[value]
  • #call メソッドがあるオブジェクトを Proc 化したいケースってどれぐらいあるんですかね

[Feature #18640] default empty string argument for String#sub and String#sub!, e.g. "hello".sub("l")

  • String#sub(pattern, replace)replace のデフォルト値を "" にする提案
# "hello".sub("l", "") と同じ意味になる
p "hello".sub("l")
# => "helo"
  • sub って名前なのに引数が1つしかないのがちょっと気持ち悪いなあ

2022/03/17 今回の気になった bugs.ruby のチケット

今週はブロックの引数に関するバグチケなどチケットがありました。

[Bug #18635] Enumerable#inject without block/symbol will return values or raise LocalJumpError

  • ブロック引数のない Enumerable#inject を呼び出すと『値が返ってくるか例外が発生するか』で一貫性がないというバグ報告
pp [].inject       # => nil
pp [1].inject      # => 1
pp [1, 2].inject   # error: `each': no block given (LocalJumpError)
  • 前者2つはイテレーションされないので『要素の最初の値』をそのまま返している感じですかね?
  • チケットを立てた人は『常に LocalJumpError (もしくは ArgumentError )を発生させたほうがいい』とコメントしている

[Bug #18632] Struct.new wrongly treats a positional Hash as keyword arguments

  • 以下のように Hash を位置引数として渡しているがキーワード引数としてエラーになっているというバグ報告
# これはキーワード引数なので意図するエラーになる
# error: `new': unknown keyword: :name (ArgumentError)
Struct.new(:a, name: "b")
# これは Hash を位置引数で渡している
# しかし、キーワード引数を渡した時と同じエラーになる
# error: `new': unknown keyword: :name (ArgumentError)
Struct.new(:a, { name: "b" })
  • また位置引数に Hash を渡すとエラーになるが、以下のように空の hash を渡すとエラーにならない
# OK
p Struct.new(:a, {}).members
# => [:a]
  • ややこしい…

[Bug #18633] proc { |a, **kw| a } autosplats and treats empty kwargs specially

  • ブロックの引数は引数が2つ以上の場合は位置引数を分割引数として値を受け取る
# これは a = [1, 2] で受け取る
p proc { |a| [a] }.call [1, 2]
# => [[1, 2]]

# これは a = 1, b = 2 と配列を分割して受け取る
p proc { |a, b| [a, b] }.call [1, 2]
# => [1, 2]
  • これが『位置引数が1つでキーワード引数がある場合も同じ挙動になっている』というバグ報告
# キーワード引数がある場合も a = 1 として配列を分割して受け取る
p proc { |a, **kwd| [a, kwd] }.call [1, 2]
# => [1, {}]
  • ちなみに実引数にキーワード引数がある場合は『分割引数として受け取らない』
p proc { |a, **kwd| [a, kwd] }.call [1, 2], c: 3
# => [[1, 2], {:c=>3}]

# 空のキーワード引数でも同様
p proc { |a, **kwd| [a, kwd] }.call [1, 2], **{}
# => [[1, 2], {}]

[Bug #18629] block args array splatting assigns to higher scope _ var

  • 以下のように仮引数が _ でない場合はスコープの外の変数は書き換わらないが _ の場合は外のスコープの変数が書き換わってしまうというバグ報告
# これは書き換わらない
v = 1; [[2]].each{ |(v)| }; p v   # => 1

# これは書き換わってしまう
_ = 1; [[2]].each{ |(_)| }; p _   # => 2
  • 『これは _ 付き変数( _ 自体も含む)が特殊な変数になっているから』とコメントされている
# 同じ名前の仮引数があるとエラーになる
def a(b, b) end   # SyntaxError

# 同じ名前でも _ が付いている場合はエラーにならない
def a(_b, _b) end # no error

[Bug #18624] const_source_location returns [false, 0] when autoload is defined for the constant

  • 以下のように autoload を設定している定数に対して const_source_location を呼び出した時に [false, 0] が返ってくるのは期待する挙動ではないというバグ報告
    • ['/path/to/test2.rb', 2] が返ってくるのが期待する挙動
# test.rb
path = File.join(__dir__, 'test2')
Object.autoload 'Test2', path
require path

p Object.const_source_location 'Test2'
# test2.rb
class Test2
end
$ ruby -v test.rb
ruby 3.2.0dev (2022-03-11T08:38:13Z master 2e4516be26) [x86_64-linux]
[false, 0]
  • これは Zeitwerk を使った場合に同様の問題があるらしい
# test.rb
require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.setup

require File.join(__dir__, 'test2')

p Zeitwerk::VERSION
p Object.const_source_location 'Test2'
# test2.rb
class Test2
end

[Bug #18620] Not possible to partially curry lambda or proc's call method

  • Method オブジェクトを #curry 化すると次のように #[] を別々に呼び出して評価できる
class Foo
  def foo(a, b)
    a + b
  end
end
Foo.new.method(:foo).curry[1][2] # => 3
  • これは proclambda でも同じ挙動になる
lambda { |a, b| a + b }.curry[1][2] # => 3
proc { |a, b| a + b }.curry[1][2] # => 3
  • しかし Proc#callMethod オブジェクト化した場合は期待する挙動にならないというバグ報告
# error: `block in <top (required)>': wrong number of arguments (given 1, expected 2) (ArgumentError)
lambda { |a, b| a + b }.method(:call).curry[1][2]
# error: `+': nil can't be coerced into Integer (TypeError)
proc { |a, b| a + b }.method(:call).curry[1][2]
  • これがバグかどうかは次の開発者会議で議論されるとの事
  • ちなみに #[] に複数の引数を渡すと正しく動作する
lambda { |a, b| a + b }.method(:call).curry[1, 2] # => 3
proc { |a, b| a + b }.method(:call).curry[1, 2] # => 3

[Bug #18561] Make singleton def operation and define_singleton_method explicitly use public visibility

  • 次のようにクラススコープで private を呼び出した後に『クラスメソッド』を定義しても private にはならない
class X
  private

  X.define_singleton_method(:hoge) do
    "hoge"
  end
end

# クラスメソッドは private ではないので呼び出せる
pp X.hoge
# => "hoge"
  • これは private になるのはあくまでも『クラスのインスタンスメソッド』になるから
  • このチケットでは『この挙動が仕様であることを明示的にドキュメントに書こう』という旨になる
  • ただし、以下のように『特定のケースでクラスメソッドが private になる』というバグ報告もされている
class X
  class << X
    private

    # 特異クラスのスコープ内で define_singleton_method を定義すると private になる
    X.define_singleton_method(:hoge) do
      "hoge"
    end

    # こっちは private にならない
    def X.foo
      "foo"
    end
  end
end

# OK
pp X.foo    # => "foo"

# NG
# error: `<main>': private method `hoge' called for X:Class (NoMethodError)
pp X.hoge

[Bug #18622] const_get still looks in Object, while lexical constant lookup no longer does

  • 次のように :: で定数参照した場合と const_get で定数参照した場合で差異があるというバグ報告
module ConstantSpecsTwo
  Foo = :cs_two_foo
end

module ConstantSpecs
end

# これは定数参照できる
p ConstantSpecs.const_get("ConstantSpecsTwo::Foo") # => :cs_two_foo

# これはエラーになる
# error: const_get.rb:9:in `<main>': uninitialized constant ConstantSpecs::ConstantSpecsTwo (NameError)
p ConstantSpecs::ConstantSpecsTwo::Foo
  • これは Ruby 2.5 で定数参照が変わった時に関連しているぽい
class C
end

# トップレベルで定義された定数は暗黙的に Object の配下で定義される
class C2
end

# なので Ruby 2.4 以前では C:: で参照する事ができていたが Ruby 2.5 からはできなくなった
p C::C2
# Ruby 2.4 => C2
# Ruby 2.5 => error: `<main>': uninitialized constant C::C2 (NameError)
  • 議論は盛り上がってるぽいけど全然追えてない…
  • 個人的にはトップレベル定数の扱いを変えたい…
    • トップレベルで定義された定数は暗黙的に private 定数になる、みたいなルールがすっきりすると思ってる

[Feature #18617] Allow multiples keys in Hash#[] acting like Hash#dig

  • Hash#dig のように Hash#[] に複数の引数を渡せるようにする提案
hash[:a][:b][:c][:d][:e][:f][:u]
hash[:a, :b, :c, :d, :e, :lov, :u]
  • と記述できるようにする提案
  • Hash#[] で対応するのであれば Array#[] とかでも対応する必要がありそう

Ruby の呼び出し可能オブジェクトについて

メタプロRuby本を参考にしつつ Ruby の呼び出し可能オブジェクトについてまとめてみました。

呼び出し可能オブジェクトとは

Ruby のブロックは『呼び出すメソッドに対して何かしらの処理を外から渡す』記法になります。

# 『配列の要素を2倍にする』という処理をブロックで記述する
pp [1, 2, 3, 4, 5].map { |it| it * 2 }
# => [2, 4, 6, 8, 10]

# 『要素から偶数の値だけを抽出する』という処理をブロックで記述する
pp [1, 2, 3, 4, 5].select { |it| it.even? }
# => [2, 4]

呼び出し可能オブジェクトとはこの『ブロックの処理(など)』を『Ruby のオブジェクト』として扱うための概念になります。 この『呼び出し可能オブジェクト』は Ruby で言うと以下の2つのクラスが存在しています。

更に Proc クラスには2つの状態があります。

  • proc
  • lambda

今回はこの Proc オブジェクトや Method オブジェクトに関して解説していきます。

Proc オブジェクト

Ruby のブロックをオブジェクトとして扱う場合は Proc オブジェクトを使用します。 これは Proc.newproc メソッドでブロックからオブジェクトを生成する事ができます。

# Proc.new に渡したブロックをオブジェクト化する
plus = Proc.new { |a, b| a + b }
pp plus.class
# => Proc

# proc は Proc.new しているのと同じ意味
plus2 = proc { |a, b| a + b }
pp plus2.class
# => Proc

この時点ではまだブロックの処理は呼び出されません。 ブロックの処理を呼び出すには Proc#call メソッドを呼び出します。

plus = proc { |a, b| a + b }

# proc に渡したブロックを呼び出す
# Proc#call に渡した引数が proc に渡したブロックに渡される
pp plus.call(1, 2)   # => 3

# 何度もブロックの処理を呼び出すことができる
pp plus.call(3, 4)   # => 7
pp plus.call(5, 6)   # => 11

この『ブロックの処理を呼び出す事』を『評価する』といいます。 またこのように『ブロックをオブジェクト化してあとから処理を呼び出すこと』を『あとで評価する』や『遅延評価』などと呼ばれます。

ブロック引数に Proc オブジェクトを渡す

Proc オブジェクトは & を付ける事で他のメソッドのブロック引数として渡すこともできます。

def hoge
  # ブロックを評価する
  pp yield(1, 2)
  # => 3
end

plus = proc { |a, b| a + b }

# & を付けて渡すことで Proc オブジェクトをブロック引数として渡すことができる
hoge(&plus)

ブロック引数は Proc オブジェクトとして受け取る

以下のようにブロックを評価する場合は yield を用いて評価する事ができます。

def hoge
  pp yield(1, 2)   # => 42
end

hoge { |a, b| a + b }

これとは別に & を付けて引数を定義する事で明示的に『ブロックをオブジェクトとして受け取る』事もできます。

# block という引数でブロックのオブジェクトを受け取る
def hoge(&block)
end

この block という変数が Proc オブジェクトとして値を受け取ります。

# block は Proc オブジェクトとして受け取る
def hoge(&block)
  pp block.class         # => Proc
  pp block.call(1, 2)    # => 3
end

hoge { |a, b| a + b }

このように明示的にブロック引数を記述することで『他のメソッドにブロック引数を渡すこと』ができます。

def foo
  pp yield(1, 2)
  # => 3
end

def hoge(&block)
  # 他のメソッドにブロック引数を渡す
  foo(&block)
end

hoge { |a, b| a + b }

ブロック引数以外に Proc オブジェクトをメソッドに渡す例

ブロック引数でなくても Proc オブジェクトをメソッドに渡すことはできます。

def hoge(obj)
  obj.call(1, 2)
end

plus = proc { |a, b| a + b }
pp hoge(plus)   # => 3

これは例えば Enumerable#find のように『複数の呼び出し可能オブジェクトをメソッドに渡したい』場合に利用します。

# 最初の3よりも大きい数を探す
# 見つからない場合は nil を返す
p [1, 2, 3, 4, 5].find { |it| it > 3 }   # => 4
p [2, 2, 2, 2, 2].find { |it| it > 3 }   # => nil

# find の第一引数に呼び出し可能オブジェクトを渡すことで『見つからなかったときの処理』を制御できる
# 見つからなかった場合に第一引数の Proc オブジェクトが評価される
p [2, 2, 2, 2, 2].find(proc { "見つかりませんでした" }) { |it| it > 3 }
# => "見つかりませんでした"

# error: `block in <main>': ないYO!! (RuntimeError)
p [2, 2, 2, 2, 2].find(proc { raise "ないYO!!" }) { |it| it > 3 }

lambda を生成する

lambdalambda メソッドで定義する事ができます。 使い方は proc メソッドと同じように lambda メソッドにブロックを渡してオブジェクト化します。

plus = lambda { |a, b| a + b }

lambda メソッドで生成したオブジェクトもまた Proc オブジェクトとなります。

plus = lambda { |a, b| a + b }

# lambda メソッドで生成したオブジェクトも Proc オブジェクトになる
pp plus.class  # => Proc

# proc と同じように Proc#call でブロックを評価する事ができる
pp plus.call(1, 2)   # => 3

また lambda-> {} という特別な記法で定義する事もできる。 ブロックの引数を定義する位置が {} の内側でないので注意する。

# lambda { |a, b| a + b } と同じ意味
plus = -> (a, b) { a + b }
pp plus.call(1, 2)   # => 3

ここで重要なのは proclambda も状態が違うだけで『両方共 Proc クラスのオブジェクトになる事』です。

proclambda の違い

proclambda は細かいところで違いがあるんですが、ここでは『引数の数が厳密かどうか』と『ブロック内で return したときの違い』に絞って説明します。

引数の数が厳密かどうか

proc の場合はブロックで定義された引数の数と実際に渡された引数の数が違っていてもエラーにはなりません。

block = proc { |a, b| [a, b] }

# 定義された引数分のを渡す
pp block.call(1, 2)   # => [1, 2]

# 定義された引数よりも多いを渡してもエラーにならない
# その場合はが切り捨てられる
pp block.call(3, 4, 5, 6)   # => [3, 4]

# 定義された引数よりも少ないを渡してもエラーにならない
# その場合はが nil になる
pp block.call(7)   # => [7, nil]

一方で lambda の場合はブロックで定義された引数の数と実際に渡された引数の数が違うとエラーになります。

block = lambda { |a, b| [a, b] }

# 定義された引数分のを渡す
pp block.call(1, 2)   # => [1, 2]

# 定義された引数よりも多いを渡すとエラーになる
# error: `block in <main>': wrong number of arguments (given 4, expected 2) (ArgumentError)
pp block.call(3, 4, 5, 6)

# 定義された引数よりも少ないを渡すとエラーになる
# error:`block in <main>': wrong number of arguments (given 1, expected 2) (ArgumentError)
pp block.call(7)   # => [7, nil]

proclambda を判定するには Proc#lambda? が利用できます。

proc_obj = proc {}
lambda_obj = lambda {}

pp proc_obj.lambda?     # => false
pp lambda_obj.lambda?   # => true

またメソッドで受け取ったブロック引数は proc として受け取ります。

def hoge(&block)
  pp block.lambda?    # => false
end

hoge {}

ブロック内で return したときの違い

ブロック内で return したときの挙動が proclambda で異なります。 proc 内で return すると『そのブロックを評価したメソッドから』抜けます。

def hoge
  block = proc {
    # ここで return すると hoge メソッドから return する
    return 42
  }

  # call を呼び出すと hoge メソッドから return してしまう
  block.call

  # なので以下の処理は呼び出されない
  pp "call 後"
end

pp hoge
# => 42

lambda 内で return すると『そのブロック内から』抜けます。

def hoge
  block = lambda {
    # ここで return するとブロック内から return する
    return 42
  }

  # call を呼び出すとブロック内の return が返ってくる
  pp block.call
  # => 42

  # 以下の処理も呼び出される
  pp "call 後"
end

hoge

以下のようにメソッドのブロック内で return する場合は気をつける必要があります。

def check
  [1, 2, 3, 4].each { |it|
    if it.even?
      # ここで return すると check メソッドから抜けてしまう
      return
    end
  }
end

Ruby では意識して lambda を使わない限りは proc としてブロックを扱うことが多いので proc の挙動に対して注意しておく必要があります。

Method オブジェクト

Method オブジェクトは『メソッドを呼び出し可能オブジェクトとして扱うためのオブジェクト』になります。 Method オブジェクトは #method という特別なメソッドを用いてオブジェクトを生成します。 また Proc オブジェクトと同様に Method#call で評価する事ができます。

class Value
  def initialize(value)
    @value = value
  end

  def plus(a)
    @value + a
  end
end

value = Value.new(3)
# 通常のメソッド呼び出し
pp value.plus(4)   # => 7

# x の plus メソッドを呼び出し可能オブジェクトとして生成する
plus = value.method(:plus)

# Proc ではなくて Method クラスのオブジェクトになる
pp plus.class   # => Method

# Method#call でメソッドを評価する
pp plus.call(6)   # => 7

#method メソッドは全てのオブジェクトで定義されているので次のように呼び出す事もできます。

# String#upcase をオブジェクト化する
upcase = "string".method(:upcase)
# upcase メソッドを評価する
pp upcase.call   # => "STRING"

# Integer#+ メソッドを Method オブジェクト化する
plus = 1.method(:+)

Method オブジェクトをブロック引数に渡す

Method オブジェクトも Proc オブジェクトと同様に & を付けることでメソッドのブロック引数に渡すことができます。

class Value
  def initialize(value)
    @value = value
  end

  def plus(a)
    @value + a
  end
end

value = Value.new(3)
plus3 = value.method(:plus)

# map メソッドの内部で Value#plus メソッドが呼び出される
pp [1, 2, 3].map(&plus3)
# => [4, 5, 6]

UnboundMethod オブジェクト

Method オブジェクトは

  • 呼び出し可能オブジェクト
  • 呼び出しを行う対象のオブジェクト

の2つの情報を保持しています。 呼び出しを行う対象のオブジェクトMethod#receiver で取得する事ができます。

upcase = "string".method(:upcase)

# upcase という呼び出しの対象が "string" オブジェクトになる
pp upcase.receiver   # => "string"

この『 呼び出しを行う対象のオブジェクト 』を Method オブジェクトから取り除いたものが UnboundMethod オブジェクトになります。 UnboundMethod オブジェクトの生成方法は2つあります。

Module.instance_method から生成する

Method.instance_method から UnboundMethod を生成することができます。

# String の upcase インスタンスメソッドの `UnboundMethod` オブジェクトを生成する
upcase = String.instance_method(:upcase)
pp upcase.class   # => UnboundMethod

UnboundMethod に対して UnboundMethod#bind を使用することで対象の 呼び出しを行う対象のオブジェクト を割り当てる事ができます。

# Module.instance_method で任意のメソッドの `UnboundMethod` を生成できる
upcase = String.instance_method(:upcase)

# bind することで Method オブジェクト化することができる
upcase_string = upcase.bind("string")
pp upcase_string.class      # => Method
pp upcase_string.receiver   # => "string"

# bind したオブジェクトに対して upcase メソッドを呼び出す
pp upcase_string.call       # => "STRING"


# 他のオブジェクトも bind することができる
upcase_ruby = upcase.bind("ruby")
pp upcase_ruby.class      # => Method
pp upcase_ruby.receiver   # => "ruby"
pp upcase_ruby.call       # => "RUBY"

また .instance_method を呼び出したクラス以外のオブジェクトを bind するとエラーになります。

upcase = String.instance_method(:upcase)

# error: `bind': bind argument must be an instance of String (TypeError)
upcase.bind(42)

Method オブジェクトから UnboundMethod オブジェクトを生成する

Method#unbind を使用すると Method オブジェクトから 呼び出しを行う対象のオブジェクト が削除された UnboundMethod オブジェクトを生成します。

upcase = "string".method(:upcase)

# unbind で UnboundMethod オブジェクトが生成される
unbind = upcase.unbind
pp unbind.class   # => UnboundMethod

# UnboundMethod#bind で別のオブジェクトを束縛できる
pp unbind.bind("ruby").call   # => "RUBY"

まとめ

  • 呼び出し可能オブジェクトとは『処理をオブジェクト化したもの』になる
    • あとから任意のタイミングで『処理』を呼び出すことができる
    • 呼び出し可能オブジェクトの『処理』を呼び出すことを『評価する』と呼ぶ
  • Ruby の呼び出し可能オブジェクトは大きく分けると2種類ある
    • Proc オブジェクト
      • ブロックをオブジェクト化した
    • Method オブジェクト
      • メソッドをオブジェクト化した
  • Proc オブジェクトには proclambda の2つの状態が存在している
    • proclambda で引数の数が厳密にチェックされるかどうかの違いなどがある
  • Method オブジェクトには UnboundMethod オブジェクトという似ているオブジェクトがある
    • UnboundMethod オブジェクトは『 Method オブジェクトから 呼び出しを行う対象のオブジェクトを取り除いた』になる

Ruby の引数の種類をまとめてみた

普段何気なく書いている Ruby なんですが実はメソッドの引数の種類って思ったよりも多くてまとめてみました。
実際自分も Ruby の AST をいじっている時に引数の種類が多すぎて混乱したのでまとめておくと色々と役に立つかなーと。
と、軽い気持ちで書いてみたんですが結構なボリュームになってしまいまsた。
引数の名称とかは調べつつ独断と偏見で書いてあるのでそこら辺はご注意ください。

諸注意

  • この記事は基本的に Ruby 3.1 を元にして動作確認しています
  • 下にも書いてあるんですが 仮引数(parameter) についての解説で 実引数(argument) の話はあんまりでてきません
  • 引数の名称は日本語に寄せていますが Ruby 界隈だと英語で書いてある事が多いかも
    • 調べる時は英語の方で調べてもらったほうが情報はでてくると思う

引数という言葉の意味とは

『引数』という言葉には厳密に言うと『仮引数』と『実引数』という2つの意味があります。

  • 仮引数(parameter):メソッドを定義する時に使う変数名の事
  • 実引数(argument):実際にメソッドに渡す(渡ってきた)値の事
# メソッド定義時の a や b が仮引数(parameter)
def plus(a, b)
  a + b
end

# 実際にメソッドを渡す値が実引数(argument)
plus(1, 2)

この記事では『仮引数(parameter)』の種類について解説していきます。
なので特に記述がない場合は『引数=仮引数(parameter)』と認識してください。

Ruby の引数の種類

Ruby の引数の種類には大きく分けで4つあります。

  • 位置引数 (positional parameter)
  • キーワード引数 (keyword parameter)
  • ブロック引数 (block parameter)
  • 転送引数 (forwarding parameter)
    • この引数は Ruby 2.7 から追加された

ここから更に細かく分けると以下のように分類されます。

  • 位置引数 (positional parameter)
    • 必須引数 (required parameter)
    • オプショナル引数 (optional parameter)
    • 残余引数 (rest parameter)
    • 後置引数 (post-required parameter)
    • 分割引数 (decomposed parameter)
  • キーワード引数 (keyword parameter)
    • 必須引数 (required parameter)
    • オプショナル引数 (optional parameter)
    • 残余引数 (rest parameter)
  • ブロック引数 (block parameter)
  • 転送引数 (forwarding parameter)

位置引数とキーワード引数の項目で細かく分類されています。
また、引数を定義できる順番は基本的には以下のようになります。

def test(
  必須引数 (位置引数),
  オプショナル引数 (位置引数),
  残余引数 (位置引数),
  後置引数 (位置引数),

  必須引数 (キーワード引数),
  オプショナル引数 (キーワード引数),
  残余引数 (キーワード引数),

  ブロック引数
)
end

分割引数は上の順番に含まれない特殊な引数なので注意してください。
また転送引数に関しても書き方が特殊なので別途記述します。
この記事ではこれらの引数について1つ1つ解説していきます。
ちなみにこの記事では引数の名前を無理やり日本語に訳していますが Ruby 界隈だと英語読みで話すことが多いかも…?

メソッドの引数情報を実行時に取得する

メソッドの引数情報は Ruby の実行時に Method#parameters で取得する事ができます。

def test(a, b = 1, *c, d, (e, f), g:, h: 2, **i, &j)
end

# method メソッドで Method オブジェクトを取得し、そこから parameters で引数情報を得る
pp method(:test).parameters
# => [[:req, :a],
#     [:opt, :b],
#     [:rest, :c],
#     [:req, :d],
#     [:req],
#     [:keyreq, :g],
#     [:key, :h],
#     [:keyrest, :i],
#     [:block, :j]]
記号 意味
:req 必須引数 (位置引数) や 後置引数 (位置引数)
:opt オプショナル引数 (位置引数)
:rest 残余引数 (位置引数)
:keyreq 必須引数 (キーワード引数)
:key オプショナル引数 (キーワード引数)
:keyrest 残余引数 (キーワード引数)
:block ブロック引数

ちなみに転送引数の場合は以下のような値が返ってきます。

def test(...)
end

pp method(:test).parameters
# => [[:rest, :*], [:keyrest, :**], [:block, :&]]

参照:https://docs.ruby-lang.org/ja/latest/method/Method/i/parameters.html

位置引数 (positional parameter)

  • メソッドに値を渡した順番に依存する形で値を受け取る引数
    • 1つ目に渡した値は1つ目の引数で受け取る
  • 位置引数には以下の定義方法がある
    • 必須引数 (required parameter)
    • オプショナル引数 (optional parameter)
    • 残余引数 (rest parameter)
    • 後置引数 (post-required parameter)
    • 分割引数 (decomposed parameter)
  • メソッドに値を渡す時に * を付けると配列が展開された状態で値が渡される
def test(a, b, c)
  { a: a, b: b, c: c }
end

# 配列の中身を展開して位置引数に値を渡す
# test(1, 2, 3) と同じ意味
pp test(*[1, 2, 3])
# => {:a=>1, :b=>2, :c=>3}

必須引数 (required parameter)

  • 必ず値を渡す必要がある位置引数
# 必ず2つの値を受け取るメソッド
def plus(a, b)
  a + b
end

# OK
# メソッドの引数:a = 1, b = 2
pp plus(1, 2)   # => 3
  • 定義した必須引数の数と渡す値の数が一致していないとエラーになる
def plus(a, b)
  a + b
end

# NG: 必要な引数が足りなくてエラーになる
# error: wrong number of arguments (given 1, expected 2) (ArgumentError)
pp plus(1)

# NG: 引数が多くてもエラーになる
# error: wrong number of arguments (given 3, expected 2) (ArgumentError)
pp plus(1, 2, 3)

オプショナル引数 (optional parameter)

  • デフォルト値を設定して定義する位置引数
  • 必須引数とは違いメソッドに値を渡さなくてもエラーにならない
    • その場合はデフォルト値が変数に代入される
# 値を渡さなかった場合は pi = 3.14159 になる
def circle_area(radius, pi = 3.14159)
  radius * radius * pi
end

# 値を渡すと pi = 2 となる
# メソッドの引数:radius = 2, pi = 3
pp circle_area(2, 3)   # => 12

# 値を渡さなかった場合は pi = 3.14159 となる
# メソッドの引数:radius = 2, pi = 3.14159
pp circle_area(2)   # => 12.56636
  • デフォルト値は他のメソッドや他の引数を参照する事もできる
def pi
  3.14
end
@value = 42

# 引数 a をデフォルト値として引数 b を定義する
def test(a, b = a, c = pi, d = @value)
  { a: a, b: b, c: c, d: d }
end

pp test(42)
# => {:a=>42, :b=>42, :c=>3.14, :d=>42}
  • またオプショナル引数を必須引数よりも後に書くと次のような挙動になる
    • オプショナル引数ではなくて必須引数を優先して値を受け取る
# 引数が1つの場合はオプショナル引数ではなくて必須引数で値を受け取る
def test(a = 1, b)
  { a: a, b: b }
end

# 引数が1つの場合は b = 5 になる
pp test(5)
# => {:a=>1, :b=>5}

# 引数が2つの場合は a = 6, b = 7 になる
pp test(6, 7)
# => {:a=>6, :b=>7}
  • そして次のように必須引数の前後にオプショナル引数がある場合はエラーになるので注意する
# error: syntax error, unexpected '=', expecting ')'
def test(a = 1, b, c = 2)
end

残余引数 (rest parameter)

  • 必須引数やオプショナル引数(や後述の後置引数)以外で渡された値を全て受け取る位置引数
    • 渡された値は配列として受け取る
  • * を付けて仮引数を定義する
# 2つ目以降に渡された値を全てを args で受け取る
# args は渡された全ての値の配列になる
# 値がなければ空の配列になる
def sum(a, *args)
  return a + args.sum
end

# 1つ以上の任意の数の引数を渡すことができる

# メソッドの引数:a = 1, args = []
p sum(1)   # => 1

# メソッドの引数:a = 1, args = [2]
p sum(1, 2)   # => 3

# メソッドの引数:a = 1, args = [2, 3]
p sum(1, 2, 3)   # => 6
  • また残余引数は仮引数名を省略する事もできる
# 第一引数以外の位置引数は受け取らない
def first(a, *)
  a
end

pp first(1, 2, 3)
# => 1
  • 更に Ruby 3.2 からは変数名を省略した場合でも別のメソッドに引数を渡せるようになる予定
def foo(a, b, c)
  a + b + c
end

# Ruby 3.1 までは変数名を書いて他のメソッドに渡す必要があった
def hoge1(*args)
  # hoge1 で受け取った値をそのまま foo メソッドに渡す
  foo(*args)
end

# Ruby 3.2 からは * だけで他のメソッドに渡すことができるようになった
def hoge2(*)
  # hoge2 で受け取った値をそのまま foo メソッドに渡す
  foo(*)
end

pp hoge1(1, 2, 3)   # => 6
pp hoge2(1, 2, 3)   # => 6

後置引数 (post-required parameter)

  • 残余引数以降に定義できる必須引数
  • 残余引数と組み合わせて定義できる
  • これを利用すると『残余引数の最後の値だけ』を受け取るようなこともできる
# 一番最後に渡された引数を last で受け取る
def last(*args, last)
  last
end

# 複数の値を渡した中の最後の引数を返す
# メソッドの引数:args = [1, 2, 3], last = 4
pp last(1, 2, 3, 4)
# => 4

# 実引数が 1つだけの場合は last で値を受け取る
# メソッドの引数:args = [], last = 42
pp last(42)
# => 42
  • 必須引数と組み合わせる事もできる
# 前後の引数の値を受け取ることできる
def first_last(first, *middle, last)
  [first, last]
end

# メソッドの引数:fisrt = 1, middle = [2, 3], last = 4
pp first_last(1, 2, 3, 4)
# => [1, 4]
  • また後置引数にはデフォルト値を設定する事はできない
# syntax error, unexpected '=', expecting ')'
def test(*args, a = 1)
end

分割引数 (decomposed parameter)

  • 配列を分割して受け取る位置引数
  • 分割引数は必須引数と後置引数の部分で記述できる
    • () を付けて仮引数を定義する
# 2つ目の値を配列の要素を分割して受け取る
def test(a, (b, c))
  { a: a, b: b, c: c }
end

# [2, 3] を b = 2, c = 3 に分割して受け取る
pp test(1, [2, 3])
# => {:a=>1, :b=>2, :c=>3}
  • 分割引数は配列の要素数が定義した仮引数の数と違っていたり配列でなくても渡すことができる
def test(a, (b, c))
  { a: a, b: b, c: c }
end

# 配列の要素数が多くても渡すことができる
pp test(1, [2, 3, 4])
# => {:a=>1, :b=>2, :c=>3}

# 逆に少ない場合は nil で埋められる
pp test(1, [2])
# => {:a=>1, :b=>2, :c=>nil}

# そもそも配列でなくても渡せる
pp test("hoge", "foo")
# => {:a=>"hoge", :b=>"foo", :c=>nil}

# ただし、3つ以上の値を渡すとエラーになる
# error: `test': wrong number of arguments (given 3, expected 2) (ArgumentError)
pp test(1, 2, 3)
  • 残余引数の受け取り方は多重代入と同じような挙動になる
# 配列の中身を分割して複数の変数に代入できる
foo, bar, baz = [1, 2, 3]
pp foo   # => 1
pp bar   # => 2
pp baz   # => 3

# * を付けると複数の値を配列で受け取る
piyo, *post = [1, 2, 3]
pp piyo   # => 1
pp post   # => [2, 3]

# 前後で分けて値を受け取る事もできる
first, *mid, last = [1, 2, 3, 4, 5]
pp first   # => 1
pp mid     # => [2, 3, 4]
pp last    # => 5
  • また分割引数では残余引数も定義できる
# () の中で残余引数も定義できる
def test(a, (b, *c))
  a + b + c.sum
end

# メソッドの引数:a = 1, b = 2, c = [3, 4]
pp test(1, [2, 3, 4])
# => 10

# こんな感じで複雑にネストして書くこともできる
def test2((a, (b, (c, *d)), e))
  a + b + c + d.sum + e
end

# メソッドの引数:a = 1, b = 2, c = 3, d = [4, 5], e = 6
pp test2([1, [2, [3, 4, 5]], 6])
# => 21
  • ただし、分割引数ではオプショナル引数は定義できない
# error: syntax error, unexpected '=', expecting ')'
def test(a, (b, c = 2))
end

キーワード引数 (keyword parameter)

  • 値を渡す時に名前を指定できる引数
    • 名前: という形で仮引数を定義する
  • 値を渡す時に名前を指定するので順番に依存せずに渡すことができる
    • 1つ目に渡した値が1つ目の引数で受け取るとは限らない
  • キーワード引数には以下の定義方法がある
    • 必須引数 (required parameter)
    • オプショナル引数 (optional parameter)
    • 残余引数 (rest parameter)
  • メソッドに値を渡す時に ** を付けると Hash が展開された状態で値が渡される
def test(a:, b:, c:)
  { a: a, b: b, c: c }
end

hash = { a: 1, b: 2, c: 3 }

# Hash の中身を展開してキーワード引数に値を渡す
# test(a: 1, b: 2, c: 3) と同じ意味
pp test(**hash)
# => {:a=>1, :b=>2, :c=>3}
  • またキーワード引数は位置引数よりも後に定義する必要がある
# OK
def test(a, b:)
end

# NG: syntax error, unexpected local variable or method
def test(b:, a)
end
  • ちなみに key => value という形で渡すこともできる
def test(a:)
  a
end

pp test(:a => 42)
# => 42

必須引数 (required parameter)

  • 必ず値を渡す必要があるキーワード引数
  • 定義した必須引数に値を渡さないとエラーになる
# 名前: という形で引数を定義する
def circle_area(radius:, pi:)
  radius * radius * pi
end

# 名前: という形で引数を渡す
# メソッドの引数:radius = 4, pi = 3
pp circle_area(radius: 4, pi: 3)   # => 48

# error: `circle_area': missing keyword: :pi (ArgumentError)
pp circle_area(radius: 4)
  • 名前を指定するので値を渡す順番に依存しない
def circle_area(radius:, pi:)
  radius * radius * pi
end

# 引数の順番が異なっても渡すことができる
# メソッドの引数:radius = 2, pi = 3.14
pp circle_area(pi: 3.14, radius: 2)   # => 12.56

オプショナル引数 (optional parameter)

  • デフォルト値を設定して定義するキーワード引数
  • 必須引数とは違いメソッドに値を渡さなくてもエラーにならない
    • その場合はデフォルト値が変数に代入される
# 名前: デフォルト値、という形で引数を定義する
def circle_area(radius:, pi: 3.14159)
  radius * radius * pi
end

# デフォルト値のあるキーワード引数は省略できる
# メソッドの引数:radius = 2, pi = 3.14159
pp circle_area(radius: 2)   # => 28.27431

# 明示的に値を渡すこともできる
# メソッドの引数:radius = 4, pi = 3
pp circle_area(radius: 4, pi: 3)   # => 48
  • オプショナル引数はキーワード引数のどの位置でも定義できる
def test(a:, b: 2, c:, d: 4)
  { a: a, b: b, c: c, d: d }
end

pp test(c: 3, a: 1)
# => {:a=>1, :b=>2, :c=>4, :d=>3}

残余引数 (rest parameter)

  • 必須引数やオプショナル引数以外で渡された値を全て受け取るキーワード引数
    • 受け取った値は Hash になる
  • ** を付けて仮引数を定義する
# ** を付けて引数を定義する
# keyword 引数を Hash として受け取る
def create(**attributes)
  attributes
end

# メソッドの引数:attributes = {:name=>"Homu", :age=>14}
pp create(name: "Homu", age: 14)
# => {:name=>"Homu", :age=>14}
  • また残余引数は仮引数を省略する事もできる
# 第一引数以外のキーワード引数は受け取らない
def first(a:, **)
  a
end

pp first(a: 1, b: 2, c: 3)
# => 1
  • 更に Ruby 3.2 からは変数名を省略した場合でも別のメソッドに引数を渡せるようになる予定
def foo(a:, b:, c:)
  a + b + c
end

# Ruby 3.1 までは変数名を書いて他のメソッドに渡す必要があった
def hoge1(**kwd)
  # hoge1 で受け取った値をそのまま foo メソッドに渡す
  foo(**kwd)
end

# Ruby 3.2 からは * だけで他のメソッドに渡すことができるようになった
def hoge2(**)
  # hoge2 で受け取った値をそのまま foo メソッドに渡す
  foo(**)
end

pp hoge1(a:1, b: 2, c: 3)   # => 6
pp hoge2(a:1, b: 2, c: 3)   # => 6

位置引数にキーワード引数を渡す

  • 位置引数でもキーワード引数を受け取れる
  • その場合は Hash としてキーワード引数を受け取る
# 位置引数でもキーワードを受け取る事ができる
def test(kwd)
  pp kwd
  # => {:a=>1, :b=>2, :c=>3}
end

# メソッドの引数:kwd = {:a=>1, :b=>2, :c=>3}
test(a: 1, b: 2, c: 3)
  • 位置引数とキーワード引数がある場合はキーワード引数が優先される
def test(a = 1, b: 2)
  [a, b]
end

pp test(b: 42)
# => [1, 42]


def test2(*args, **kwd)
  [args, kwd]
end

pp test2(a: 1, b: 2)
# => [[], {:a=>1, :b=>2}]
  • また **nil と書くことで『明示的にキーワード引数を受け取らない』と定義することもできる
    • この書き方は Ruby 2.7 で追加された
def test(a = 1, **)
end

# OK
test(b: 42)

def test2(a = 1, **nil)
end

# NG: no keywords accepted (ArgumentError)
test2(b: 42)

キーワード引数の非互換な変更について

  • Ruby 3.0 からキーワード引数の仕様が変更された
  • Ruby 3.0 以前では次のように Hash オブジェクトをキーワード引数として渡すことが可能だった
def test(a:, b:)
  a + b
end

hash = { a: 1, b: 2 }
p test(hash)
# => 3
  • しかし Ruby 3.0 からはこれがエラーになるようになった
def test(a:, b:)
  a + b
end

hash = { a: 1, b: 2 }

# eror: wrong number of arguments (given 1, expected 0; required keywords: a, b) (ArgumentError)
p test(hash)
  • Hash オブジェクトをキーワード引数として渡したい場合は ** を付けることで渡すことができる
def test(a:, b:)
  a + b
end

hash = { a: 1, b: 2 }
p test(**hash)
# => 3

ブロック引数 (block parameter)

  • ブロックを受け取る事ができる引数
  • 受け取った値は Proc オブジェクトになる
    • Prco#call でブロックの中の処理を呼び出すことができる
# &名前 でブロック引数を受け取ることができる
def test(&block)
  pp block
  # => #<Proc:0x000056003b763b58 /path/to/test.rb:11>

  # call メソッドでブロックの処理を呼び出すことができる
  block.call(1, 2)
end

# メソッドの引数:block = #<Proc:0x00007f03114f59c8 /path/to/test.rb:11>
pp test { |a, b| a + b }
# => 3
  • ブロック引数は必須引数ではないので引数がない場合は nil になる
def test(&block)
  block
end

# ブロック引数がない場合は nil になる
pp test {}
# => nil
  • またブロック引数は1つだけしか定義できない
# ブロック引数は複数定義できない
# error: syntax error, unexpected ',', expecting ')'
def test(&block1, &block2)
end
  • Ruby 3.1 からはブロック引数は仮引数を省略する事もできる
  • その場合は & だけで他のメソッドにブロック引数を渡すことができる
def hoge(&block)
  block.call
end

def foo(&)
  # & だけでブロック引数を他のメソッドに渡せる
  hoge(&)
end

pp foo { 42 }
# => 42

ブロック引数の仮引数を省略する

  • ブロック引数の仮引数を省略した場合に yield というキーワードでブロックの処理を呼び出すことができる
def test
  # test(&block) で受け取って block.call(1, 2) しているのと同じ意味
  yield(1, 2)
end

pp test { |a, b| a + b }
# => 3
  • #block_given? で『ブロック引数が渡されたかどうか』を判定して呼び出しを切り分ける事もできる
def test
  if block_given?
    "ブロック引数がある"
  else
    "ブロック引数がない"
  end
end

pp test     # => "ブロック引数がない"
pp test {}  # => "ブロック引数がある"

ブロック引数に & で値を渡す

  • ブロック引数に値を渡す場合に & を付けて渡すことができる
# ブロック引数に &:upcase を渡している
pp %w(homu mami mado).map(&:upcase)
# => ["HOMU", "MAMI", "MADO"]
  • &obj でブロック引数に値を渡した時は obj.to_proc の値をブロック引数として受け取る
def test(&block)
end

# :upcase.to_proc を block で受け取る
pp test(&:upcase)
  • なので最初のコードは以下と同じ意味になる
# :upcase.to_proc.call(it) は it.upcase と同じ事をしている
pp %w(homu mami mado).map { |it| :upcase.to_proc.call(it) }
# => ["HOMU", "MAMI", "MADO"]

転送引数 (forwarding parameter)

  • 全ての引数を受け取って他のメソッドに渡すことができる引数
    • ... という記述で引数を受け取る
  • Ruby 2.7 から追加された新しい引数
def circle_area(radius, pi: 3.14159)
  radius * radius * pi
end

# ... と書くことで全ての引数を受け取る事ができる
def debug_circle_area(...)

  # 受け取った引数を全て別の引数に渡すことができる
  pp(...)
  circle_area(...)
end

pp debug_circle_area(3, pi: 3.14)
# => 3
#    {:pi=>3.14}
#    28.26
  • また Ruby 2.7.3 からは test(name, ...) のように位置引数と組み合わせる事もできる
# name だけを位置引数として受け取る事ができる
def test(name, ...)
  pp name
  # => "homu"

  pp(name, ...)
  # => "homu"
  # => {:age=>14}

end

test("homu", age: 14)
  • ただし、キーワード引数と組み合わせる事はできないので注意
def test(name: , ...)
end

ブロックの引数について

  • ブロックの引数でも基本的にはメソッドの引数と同じように定義する事ができる
block = proc { |a, b, c = 3, (d, e), f:, g: 7, **h, &i|
  [a, b, c, d, e, f, g, h, i.call]
}

pp block.call(1, 2, [4, 5], f: 6, h: 8, j: 9) { 10 }
# => [1, 2, 3, 4, 5, 6, 7, {:h=>8, :j=>9}, 10]
  • ただし、以下のケースでメソッドの引数とは異なる点がある

ブロックの引数はデフォルトで『分割引数』として値を受け取る

  • ブロックの引数はデフォルトでは『分割引数』として受け取る
  • なので引数が2つ以上あるのブロックに配列を渡すと次のような挙動になる
# 配列ではない場合は普通に受け取る
pp proc { |a, b| [a, b] }.call 1, 2
# => [1, 2]

# a, b は分割引数として定義される
pp proc { |a, b| [a, b] }.call [1, 2]
# => [1, 2]

# 混じってる場合は配列として受け取る
pp proc { |a, b| [a, b] }.call [1, 2], 3
# => [1, 2], 3
  • また仮引数が1つだけの場合でも末尾に , が付いていると分割引数になる
# , がない場合は分割引数にならない
pp proc { |a| a }.call [1, 2]
# => [1, 2]

# , がある場合は分割引数になる
pp proc { |a,| a }.call [1, 2]
# => 1
  • これは Hasheach する時などに注意する必要がある
hash = { a: 1, b: 2, c: 3 }

# ブロックの引数が引数が一つの場合は配列で受け取る
hash.each { |it| pp it }
# => [:a, 1]
#    [:b, 2]
#    [:c, 3]

# ブロックの引数が2つの場合は配列を分割して受け取る
hash.each { |key, value| pp [key, value] }
# => [:a, 1]
#    [:b, 2]
#    [:c, 3]

仮引数よりも少ない数の値を渡す事ができる

  • ブロックの引数は仮引数よりも少ない数の値を渡す事ができる
# 仮引数は2つだが、実引数を1つだけ渡すことができる
# その場合、値を渡されなかった仮引数は nil になる
pp proc { |a, b| [a, b] }.call(1)
# => [1, nil]
  • ただし lambda-> {} でブロックが定義されている場合は引数の数が厳密にチェックされる
    • 必須引数になる
# error: wrong number of arguments (given 1, expected 2) (ArgumentError)
pp lambda { |a, b| }.call(1)

転送引数には未対応

  • ブロックの引数では転送引数を定義できない
# syntax error, unexpected (..., expecting '|'
block = proc { |...|
  foo(...)
}

引数の定義の仕方まとめ

名前 書き方 説明
必須引数 (位置引数) def test(a, b) 必ず必要な位置引数
オプショナル引数 (位置引数) def test(a = 1, b = 2) 実引数がない場合はデフォルト値が代入される引数
残余引数 (位置引数) def test(*args) 複数の位置引数を受け取ることができる引数
後置引数 (位置引数) def test(*args, b) 残余引数以降にかける必須な引数
分割引数 (位置引数) def test((a, b)) 配列を分割して受け取る事ができる引数
必須引数 (キーワード引数) def test(a:, b:) 名前を付けて渡せる引数
オプショナル引数 (キーワード引数) def test(a: 1, b: 2) 実引数がない場合はデフォルト値が代入される引数
残余引数 (キーワード引数) def test(**kwd) 複数のキーワード引数を受け取ることができる引数
ブロック引数 def test(&block) ブロック引数を受け取る特殊な引数
転送引数 def test(...) 引数を全て受け取り他のメソッドに渡すことができる特殊な引数

引数の渡し方まとめ

名前 渡し方 説明
位置引数 test(1, 2) 仮引数の順番に依存して値を渡す
splat 引数 test(*[1, 2]) 配列を展開して位置引数に値を渡す。 test(1, 2) と同じ意味
キーワード引数 test(a: 1, b: 2)test(:a => 1, :b => 2) 名前を指定して値を渡す
double splat 引数 test(**{ a: 1, b: 2 }) Hash を展開してキーワード引数に値を渡す。 test(a: 1, b: 2) と同じ意味
ブロック引数 test { }test do end {}do end の中のコードを Proc オブジェクトとしてブロック引数に値を渡す
ブロック引数 (&渡し) test(&obj) obj.to_proc の結果をブロック引数の値として渡す

NOTE: splat 引数や double splat 引数は日本語だとなんて言えばいいんですかね…

まとめ

軽い気持ちでまとめようと思ったら思った以上に大ボリュームなまとめになってしまいました。
もっとさくっとかけると思ってたんですが思ったよりも Ruby の引数って種類が多かったんですね、結構細かいところまで詰めて書いたつもりなんですがこれでも多分まだ全然足りない気がする…。 特にキーワード引数周りの細かい仕様とか(Ruby 3.0 の非互換な話とか)引数の転送の話とかキーワード引数の渡し方とか etc...。
機会があればまた足りない部分は補足したいなあ。

参照