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 でサポートされる予定です。
Ruby の Unicode 14.0.0 対応
Ruby の Unicode 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.mk
の UNICODE_VERSION
と UNICODE_EMOJI_VERSION
のバージョンが 14
に更新されています。
UNICODE_EMOJI_VERSION
が別にあるんですね?
また作業当時はまだ beta 版だったので UNICODE_BETA = YES
となっているぽい?
他には enc/unicode/14.0.0/casefold.h
と enc/unicode/14.0.0/name2ctype.h
の2つのファイルが新しく追加されて lib/unicode_normalize/tables.rb
が更新されています。
このあたりのファイルはあとで詳しくみていきましょうかね。
[8e1f3a96ae] switch UNICODE_BETA back to NO
common.mk
の UNICODE_BETA
が NO
になっています。
[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_VERSION
と UNICODE_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.h
と enc/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.rb
は enc/unicode/14.0.0/casefold.h
と enc/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
を元にして機械的に生成されているぽいですかね?
まとめ
と、いう感じで簡単にですが Ruby が Unicode 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.new
やRegexp.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]
を取得したい』という要望のチケット :undefined
の情報を取得する手段としてModule#undefined_instance_methods
が新しく追加される予定- PR: https://github.com/ruby/ruby/pull/5733
- まだマージはされていないが matz は accept 済み
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#parameters
にlambda: 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], {}]
- Ruby むずかしいな?
- 修正PR は既にできてる
[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
- これに対する修正PR は既にでてる
autoload
ってconst_source_location
でも発火するのが期待する挙動になるのか
[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
- これは
proc
やlambda
でも同じ挙動になる
lambda { |a, b| a + b }.curry[1][2] # => 3
proc { |a, b| a + b }.curry[1][2] # => 3
- しかし
Proc#call
をMethod
オブジェクト化した場合は期待する挙動にならないというバグ報告
# 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
- 後者の方は修正PR が投げられている
[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)
[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.new
や proc
メソッドでブロックからオブジェクトを生成する事ができます。
# 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
を生成する
lambda
は lambda
メソッドで定義する事ができます。
使い方は 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
ここで重要なのは proc
も lambda
も状態が違うだけで『両方共 Proc
クラスのオブジェクトになる事』です。
proc
と lambda
の違い
proc
と lambda
は細かいところで違いがあるんですが、ここでは『引数の数が厳密かどうか』と『ブロック内で 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]
proc
と lambda
を判定するには 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
したときの挙動が proc
と lambda
で異なります。
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
オブジェクトにはproc
とlambda
の2つの状態が存在しているproc
とlambda
で引数の数が厳密にチェックされるかどうかの違いなどがある
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)
キーワード引数の非互換な変更について
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
- キーワード引数の変更に関しては以下の記事を参照してください
- Ruby 3.0における位置引数とキーワード引数の分離について
ブロック引数 (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
- これは
Hash
をeach
する時などに注意する必要がある
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...。
機会があればまた足りない部分は補足したいなあ。
参照
- norswap · Ruby Methods, Procs and Blocks
- クラス/メソッドの定義 (Ruby 3.0 リファレンスマニュアル)
- Ruby 3.0における位置引数とキーワード引数の分離について
- Ruby 3: 引数をforwardする
…
記法が第2パラメータでも使えるようになった(翻訳)|TechRacho by BPS株式会社 - Defining methods - Ruby Reference
- Ruby adds support for forwarding arguments to a method, along with the leading arguments | Saeloun Blog