Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby研究シリーズ1: メソッド定義構文はどう決定され、どう進化したか(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。また、主にRuby以外のユーザーに向けたサイドストーリー的なパラグラフは囲みスタイルにしています。

Ruby研究シリーズ1: メソッド定義構文はどう決定され、どう進化したか(翻訳)

本記事は、新シリーズ記事のパート1であり、おそらく書籍の一部として収録されることが見込まれています。Rubyプログラミング言語が設計されたときのさまざまな決定事項と、それらが時とともにどのように進化したか、より広い文脈ではどのように捉えられるかについて研究します。

今回はメソッド定義(method definitions)、つまりメソッド定義の一般的な形と、引数の指定方法を扱います。
このトピックは比較的小さなものに見えるかもしれませんが、ここを研究することで言語の広大な設計空間を見通せるようになり、初期のRuby言語における設計上の決定が以後のRubyの進化にどう影響したかを明確に把握できるようになります。

また、オブジェクト指向言語の研究であるにもかかわらず、最初にメソッド定義を選ぶのはいささか奇妙に思われるかもしれませんが、Rubyの事例を研究するうえではこれが適しています。皆さんにその理由を理解いただけるようになれば幸いです。

原注

Rubyではあらゆる関数がメソッドです。つまり、一見そうでなさそうに見えても、メソッドの背後には必ず何らかのオブジェクトがあるのです(これについてはシリーズの次回で取り上げる予定です)。ただし本章で解説する設計上の疑問点のほとんどは一般に通用するものであり、オブジェクトという概念がまったくない言語の範囲内でも理解可能です。

🔗 基本的なメソッド定義構文と、その背後にあるもの

言語の設計でメソッド定義の構文を決定する場合にどんな選択肢があるのかを知るために、最初はできるだけシンプルなものを基準とすることにしましょう。

def log(text)
  # ここにメソッド本文が置かれる
  #(おそらくコンソールに何か出力するだけ)
end

上は、主流のプログラミング言語で既にお馴染みの構文です。
このあたりは、記号の使い方を別にすればどの言語でも基本的に同じですが、Rubyで使われる記号も「キーワード」「メソッド名」「丸かっこ()」「引数名」「カンマ,による区切り」というように、主流の言語で普通に使われているものばかりで、むしろありふれているとすら言えます。
Rubyは「奇妙で風変わりな言語」と称されがちですが、むしろ基本的な構文レベルでは難解ではない(non-esoteric)言語という原則に沿っていると言えます。

しかし、ありふれた構文を採用しても、近い言語やライバル言語、先行言語の動向に影響される形で(または言語メンテナーの思いつきで)、他の主流言語と同様にさまざまな疑問点や設計上の決定が生まれてくるものです。

たとえば、「オプション引数はどうやって指定するか?」「デフォルト値はどのように宣言するか?」(さらに言えば、「オプション引数のデフォルト値とはそもそも何なのか?」あるいは古典的なスクリプト言語のように「全引数をオプショナルにするか?」など)といった疑問が生じるかもしれません。
また、「型は静的に定義するか、それとも段階的型付け言語のように定義するか?」「型アノテーションは他にどんな方法が可能か(有効期間、値渡し/参照渡し、イミュータブルにするかどうかなど)?」という疑問も生じるでしょう。

こうした疑問への回答はどんなものであっても、言語の構文やセマンティクス(意味論)に長期的な影響を及ぼすでしょう。さらに、上のリストはまったく網羅的ではありません(どの疑問も引数シグネチャに関するものばかりで、関数やメソッドの他の性質については言及していません)。

生まれたばかりの若い言語は、こうした疑問に回答するためにどんな手段を取れるでしょうか?

言語設計者が「言語の人間工学」をメインテーマに据えている場合は、どの決定を下すときにも、メソッド呼び出しの構文がどんな形になり、どう振る舞うかという文脈を押さえて検討する必要があります。「快適に読み書きできるか?」「書かなければならない、わかりきった詳細が多すぎないか?」「(コードを読み書きするときに)引数の意味や用法が自然にわかるか?」「タイプミスや構文ミスのエラーメッセージは有用かつわかりやすいか?」

初期の「古典的なスクリプト言語(BashやPerlやJavaScriptなど)」は、入力量が少なくチェックのユルい構文を好む傾向がありましたが、それと引き換えに、間違いを見つけて理解するのが難しくなりました。この性質は、「スクリプト」を使い捨てのグルーコード(つなぎのコード)として書くために「スクリプト」言語を利用している分には、容認可能なトレードオフと言えました。

Rubyはこの文化から生まれたと見ることが可能ですが、この「ユルい」設計が後に興味深い結果をもたらしたのです。

🔗 ウクライナ通信🇺🇦

ほんの少しお時間をください。私が生活しているウクライナが現在も侵略を受けていることを思い出していただくため、記事の途中にはさむことにしています。

あるニュース: 6月12日に、ロシアはクルィヴィーイ・リーフの住宅街を砲撃し、9人が死亡、29人が負傷しました。

ある文脈の断片: 1年前の2023年6月6日、ロシアは巨大なカホフカダムを破壊し、死者とダム損壊、生態系破壊を引き起こしました。その1年後の結果については以下の記事をご覧ください。

A Year Ago, Russia Destroyed the Kakhovka Hydroelectric Power Plant. Today, People Still Feel The Consequences | UNITED24 Media

ある募金活動:「Polubotok Treasury」1は、ウクライナ軍のいくつかの部隊に必要な装備のために、緊急の小規模募金活動を実施中です。


引き続き記事をどうぞ。

🔗 構文の柔軟性: 丸かっこが省略可能であるとはどういうことか

メソッド呼び出し構文における一見ささやかな決定事項が、言語全体の設計に後々まで大きく影響することがあります。

Rubyの場合、メソッド呼び出し側の丸かっこ()は省略可能になっているので、log("text")log "text"はまったく同じになります。特に、丸かっこなしで書いたfooという単独の名前は、丸かっこありのfoo()と同じです2

この丸かっこ()省略可能構文は、見た目優先のお飾り的な(かつ疑問が残る)決定に見えるかもしれませんが、実は言語のコンパクトかつ「柔軟な」コア機能となっているのです。これにより、言語を構成する要素のほとんどが、実際には単なるメソッド呼び出しになります。

class Foo
  attr :bar

  private

  def some_private_method
    # ...
  end
end

foo = Foo.new
foo.bar

上のコードは、主流言語のユーザーから見ても十分に伝統を踏まえた構文に見えますが、以下の理由から、言語を構成する分割不可能な要素が他の言語と比べてずっと少なく済んでいるのです。

  • attrキーワードは、実際には(まるで属性のような)インスタンスメソッドを作成する(Classクラスの)単なるメソッドである。

  • foo.barもメソッド呼び出しである。
    (Rubyには「オブジェクトの属性」という独立した概念はなく、不透明なオブジェクトとメソッドだけがある。これは、JavaScriptやPythonのような「オブジェクトは辞書であり、その一部の属性が呼び出し可能である」というやり方と真逆である)。

  • privateキーワード(以後の行にあるすべてのメソッドをprivateにする)も、実際には単なるメソッドである。

  • Foo.newは、あらゆるクラスに備わっているnewメソッドを呼び出しているに過ぎない。

原注

ただし、実はメソッド「ではない」キーワードもあります。classdevifwhileはコアキーワードであり、メソッド呼び出し構文に沿っていません。繰り返しますが、Rubyは概念を純粋にすることよりも「難解にしない」ことを尊びます。ただし、たとえば数学演算子や添字演算子は「メソッド」なので、a + bは実際にはa.+(b)と同等です(もちろん、Rubyでは +をメソッド名として利用可能です)。つまり、演算子の実装をカスタマイズしたければ、カスタムクラスでdef +(引数)を定義すればよいのです。

そういうわけで、この「すべてはメソッドであり、丸かっこの有無は関係ない」という決定によって、Rubyの柔軟性の高さは既にほぼLISP並みに達しています。ユーザーが定義するメソッドは、まるでRubyネイティブであるかのように感じられ、構文と深く統合されているように見えます。たとえば、Railsで有名な以下の「モデルマクロ」を見てみましょう。

class User < ActivRecord::Base
  has_many :posts

  validates_presence_of :name

  # ...

これを実現するために、Ruby言語の構文や定義に何ら手を加える必要はありません。has_manyvalidates_presence_ofも、ActivRecord::Baseオブジェクトが持つ通常のメソッドであり、Rubyのメソッドが呼び出されるのと同じように呼び出されます。

原注

Rubyの「呼び出し側」構文におけるもうひとつの大きな決定は、メソッドに追加される「コードブロック」という概念です。これによって、array.each { |item| ...のようなイテレータも、File.open("README.txt") do |file| ...のようなコンテキストマネージャも、「単なるメソッド呼び出し」として統一的に扱えるようになります。ただし、Rubyのブロックは大きなトピックなので、設計上の決定や進化については別記事で取り上げる予定です。

🔗 引数ラベルと波かっこ{}

「呼び出し側の利便性」と、メソッド定義構文がそこに与える影響の話に戻るとしましょう。
多くの言語は遅かれ早かれ、呼び出しの引数を順序ベースからfoo(param1 = value1, param2 = value2)のように名前ベースで結びつける必要に迫られます。

このように呼び出しで引数を名前ベースで指定する構文が好ましい理由はいくつもあります。

  • パラメータが複数ある場合に、途中のパラメータに適切なデフォルト値があれば省略できる。

  • 順序ベースだと手がかりがなくて覚えにくいようなパラメータでも、名前ベースなら覚えやすい。

  • 呼び出し側でパラメータの意味が明確になる(特にブール値パラメータは値だけだと意味が取りにくい)。

初期のRuby(Wikipediaによると最初のリリースはRuby 0.95だそうです)では、辞書リテラルを囲む波かっこ{}を省略可能にするというシンプルな方法で名前付きパラメータを解決していました。

File.open('log.txt', :mode => 'r', :encoding => 'UTF-8')

上は、実際には以下の単なるシンタックスシュガーです。

File.open('log.txt', {:mode => 'r', :encoding => 'UTF-8'})

このopenメソッドのシグネチャは、以下のようにシンプルなものになっています。

def open(filename, options)

原注

Rubyにあまり詳しくない方向けの説明:
上のコードの:mode:encodingは、Rubyのシンボル記法です。シンボルは文字列に似ていますが、内部識別子を表現するのに用いられるイミュータブルな型です。
{key => value}は辞書リテラルですが、Rubyでは歴史的な理由で辞書ではなくハッシュ(Hash)と呼ばれています。私が記事を書くときは業界で共通の名前を使うことがほとんどですが、「ハッシュ」については必ずしもそうではありません。

Rubyでは、名前付き引数を実現するために波かっこ{}を省略可能にするという最小限の概念で済みますが、この方法を嬉しいと思う人もいるでしょうし(Rubyを学び始めた頃の私がそうだったように)、逆に波かっこ{}なしの辞書リテラルが「独立したパラメータの集まりのように見える」のが目ざわりに感じる人もいるでしょう。

いずれにしろ、この方法は多くのAPIにインスピレーションを与えるほどうまくいき、Ruby 1.9(2007年)に辞書リテラルのキーをsym: "value"のようにシンボルキーで表現するシンタックスシュガーが導入されたことでさらに改善されました。そのおかげで、以下のように名前と値だけを渡して呼び出しをシンプルに書けるようになりました。

File.open('log.txt', mode: 'r', encoding: 'UTF-8')

興味深いことに、シンボルキーを導入するという決定に対するコミュニティの反応は、賛否が極端に分かれました。
曰く、シンボルキーによって確かに書きやすくはなったが、従来のkey => value記法には(キーや値がどんな型であっても)辞書であるという概念が明確に示されていたのに、その概念が損なわれてしまうというのです。実際、現在でもこのシンタックスシュガーを避けたがるRubyistが何人もいます。

しかし以下に述べるように、どうやらこのシンタックスシュガーがもたらした感覚が、Rubyでさらなる進化への道を開いたようなのです。

原注
さらに別の興味深い点は、Rubyの新しいシンボルキー構文が、JavaScriptのみならずSmalltalkをも連想させることです。
Smalltalkのメソッドシグネチャでは、コロン:付きのキーワードを利用する書き方だけが可能です。Smalltalkの構文では(方言は除外しますが)、上のメソッド呼び出しは以下のように書けます。

File open: 'log.txt' mode: 'r' encoding: 'UTF-8'

上のopen:はメソッド名でもあり、この呼び出しでは第1引数を指定する名前でもあります。ただしRubyは、Smalltalk APIのこうした部分はあまり強く受け継ごうとしませんでした。Rubyコアメソッド名の多くは当時のSmalltalkから借用していました(たとえば、業界で一般的なmapreduceではなく、Smalltalk由来のcollectinjectが使われました)が、初期のRubyでは「オプション辞書」に強く依存したコアAPIや標準ライブラリAPIは多くありませんでした。

もちろん、キーバリューのコレクション型を呼び出しの名前付き引数に転用した言語はRubyだけではありません(が私の知る限り、波かっこ{}省略シンタックスシュガーを採用したのは、少なくとも主流言語ではRubyしかないようです)。
JavaScriptもそうした明らかな例の1つですし、あまり知られていないかもしれませんがLua言語もそうです(Luaは公式ドキュメントのNamed Argumentsで解説しています)。Zigなど一部の言語では、構造体を用いたanonymous struct literalsという類似のソリューションが用いられています。

しかしPython、C#、Scalaなど、他の(主流)言語のほとんどは、引数名を書くかどうかを呼び出し側に任せる設計を選びました。

# メソッド定義側: 引数のリストを書くだけ
def open(filename, mode, encoding):
  ...

# 呼び出し側: 以下のどの呼び出しも正しい(どの引数も名前付きにできる)
open('log.txt', 'r', 'UTF-8')
open('log.txt', 'r', encoding='UTF-8')
open(encoding='UTF-8', mode='r', filename='log.txt')

原注

構文の「シンメトリー」に関するメモをここに書きたくてたまりません。

PythonやScalaやKotlinなどの言語では、メソッド呼び出しで引数名と値を紐づけるのに代入記号=を使うことを好みます(これにより、変数代入における束縛記法や、メソッド定義側のデフォルト値指定の記法とも一貫します)。

一方、C#(2010リリースの4.0以降)やPHP(2020年の8.0)などの言語では、(おそらく当時普及していたJavaScriptの記法を踏襲する形で)name: value記法を好んでいます。

JavaScriptやRubyの最小限の概念による方法は、おそらく動的言語のタイムラインにおける位置づけの産物だったのでしょう。これは当時、あくまで手軽なスケッチで考えるためのツールでした。こうしたツールは、表現力が豊かで概念がシンプルでなければならず、「スケッチで考える」が優先されたためエラー処理や堅牢性は二の次だったということです。

しかし時代が変わって動的言語が大規模システムの構築に使われるようになると、「柔軟かつ自由にお試しできる」自由を欲しがる人と、静的型付けやコンパイルによる「鉄壁の信頼性」を欲しがる人の間で論争が絶えませんでした(私はこの種の議論に参加するつもりは毛頭ありません。私は近所の言語や遠くの言語を興味本位で横目に見つつ、「そうした言語の1つ」がどんなふうに進化を遂げたかを観察しているに過ぎません)。

要するに、言語に求められるものや言語が取り組む課題は、昔とは変わってきたのです。というか、コードで手早く柔軟に考えるという行為は昔と同じですが、現代のコードが変わっただけなのです。

🔗 柔軟であっても壊れてはいけない

Rubyでは、上述の({}省略による)「名前付き引数っぽい」手法でメソッドを作成するのが非常に手軽で便利だったため、特にRailsや構造化Webアプリが動的言語全般で台頭してきたときに、ライブラリやアプリケーションでこの構文が使われるようになったのは、ほぼ必然でした。

構造が複雑で再利用可能なコードが長期間使われるようになると、(従来のスクリプト言語の使われ方である)使い捨てのスクリプトや小規模な実験的ライブラリの時代よりも引数リストが長くなり、引数を明確に記述する方法が以前よりも強く必要とされます。

しかし、この記法を支える概念がシンプルすぎたという欠点があったため、メソッド呼び出し側は自由気ままに自分の考えを表現できる一方で、メソッド作成側の負担が非常に大きくなってしまいました。

単なる例として、上述のFile.openメソッドを取り上げてみましょう。
このメソッドのmode:引数は意味論的に省略不可で、encoding:引数には "UTF-8"という適切なデフォルト値があるとします。これをRubyで完全に実装しようとすると、以下のような形で書くことになるでしょう3

def open(path, options)
  raise ArgumentError, "mode is required" unless options.key?(:mode)
  mode = options[:mode]
  encoding = options[:encoding] || 'UTF-8'
  # ...
end

原注

ただし上のチェックも完全ではありません。許可済みのキーだけがオプションハッシュに含まれていることまでは保証されないので、enocding:オプションを渡すときにオプション名をタイプミスしたのを見逃したり、まったく認識されない無効なオプションを渡してしまうといったことが起きがちです。

必要なすべてのチェックやデフォルトを適切に実装しようとすると、本当に書きたいことを書く前に形式張った定型文を書かされている気分になって、本来の考えが邪魔されてしまいがちです。その結果、メソッド本文よりも引数チェックの方が肥大化しがちな小さなメソッドほど、「チェックは後回しにしよう」という誘惑にかられるのが常です。

また、すべてのオプション引数を末尾の1個の辞書引数で受ける場合、そのメソッドを使う人は、使ってよいオプション引数はどれなのか、必須引数はどれなのかをどうやって見分ければよいのでしょうか?
こうなると、メソッド作者はAPIドキュメントを事細かに書いてエラーメッセージもうんと親切にしてあげなければならず、メソッドを使う側もメソッドのコードを隅々まで全部読んで理解しなければならなくなる、というふうにメソッド作者と利用者の双方に余分な負担を強いてしまいます。

JavaScriptは最終的に、関数定義に分割代入構文が導入されました。これは2015年のことであり、かつ問題の一部にしか答えていません。

function open(path, {mode, encoding = 'UTF-8'}) {
  // `mode`と`encoding`はローカル変数として利用可能
}
open('test.txt', {})

上の呼び出しでは、利用可能なオプションのセットが可視化され、encoding変数のデフォルト値も設定されます。しかし、modeundefinedのままになっていても、何も渡されなかったことが警告されません。

Rubyがこの問題を解決するために選択した上述の方法は、「波かっこ{}を省略すると、ラベル付き引数があたかも本物の独立したエンティティのように見える」という事実に導かれたものでした。これは見ようによっては、(本来脇役である)シンタックスシュガーが、設計上の大きな決定を可能にしたと言えるかもしれません。

Ruby 2.0(2013年2月)で導入された「本物の」キーワード引数は、それを実現する論理的なステップでした。実装の複雑さという点では大きなステップでしたが、プログラマーのメンタルモデルを調整するという点では小さく済んだステップでした。

# 以下のようなメソッド呼び出しがあるとする
File.open('test.txt', mode: 'r', encoding: 'UTF-8')

# このときメソッド定義側でも同じ構文が使えるか?
def open(path, mode: 'r', encoding: 'UTF-8')
  # `mode`と`encoding`は本体でローカル変数として利用可能
end

ここには意味論的なトリックはありません。Rubyの新しい構文は、「実は単なる辞書である」ものをシンタックスシュガーにしたのではなく、従来の慣習と一致する慣れ親しんだ書き方に見えるものの、まったく新しい概念だったのです(従来の最小概念が失われることについてコミュニティからの反発は避けられませんでしたが)。

「キーワード引数(keyword arguments: Rubyでは公式にこう命名されました4)」最初にが導入されたとき、すべてのキーワード引数にはデフォルト値が必須だったため、辞書構文のときに親しんでいたkey: "value"という書き方と常に一貫していました。

Rubyに必須キーワード引数(デフォルト値なし、省略不可)が導入されたのは、次のバージョンであるRuby 2.1(2013年12月)のことでした。そしてこのときも、構文は自然な形で決定されました。つまり、デフォルト値ありの引数がname: default_valueという形なら、デフォルト値なしの構文はどんな形になるでしょうか?

def open(path, mode:, encoding: 'UTF-8')
  # ...
end

原注: シンメトリーに関するメモ

Rubyに必須キーワード引数が導入されて以来、メソッド呼び出し側とメソッド定義側の間の「構文のシンメトリー」は不完全になりました。デフォルト値のない引数名(name:など)は、定義側では正しくても呼び出し側では構文エラーになりました。
Ruby 3.1(2021年)に以下のような引数の値省略が導入されて、ようやくシンメトリーは回復しました。

# `mode`というローカル変数がコードにあり、既に値が算出済みだったとする
mode = ...

# この場合、以下のように書かなくても
File.open('test.txt', mode: mode)

# 以下のように書ける
File.open('test.txt', mode:)

# 意味はどちらも「`mode`を、同じ名前を持つパラメータの値として使う」

(興味深いことに、この新しい構文が導入された当時は「不完全なコードに見える」と大きな反発があったのです)

この開発パスは、全般にRuby言語の精神を守っていて、引き続き初心者にとって「筋が通っている」ように見えました。そしてRubyは「関数定義で位置引数と名前付き引数を分ける言語」という小さなグループに分類されました。

これと同じような決定を下した他の言語には、以下のようなものがあります。

  • OCamlのlabeld arguments:
    2000年にリリースされたv3.0以来、主流の開発者たちにとって最も古くからある最も難解な構文のひとつです。

  • より主流に近いSwift(2014年):
    すべての引数がデフォルトで名前付き引数になっており、「一部の」引数だけを位置引数にする特殊構文(_があります。

  • Dartのnamed parameters:
    すべての名前付き引数を波かっこ{}で囲み、それ以外の構文は変更しないという興味深い構文決定を下しています。これはRubyの初期設計で波かっこ{}を省略したのと対照的なのが面白い点です。

  • Rakuの位置引数と名前付き引数の扱い:
    RakuはPerlの魅力的な子孫であり、新構文のアイデアを試す設計ラボのように感じられることもあります。特に名前付き引数で関数を呼び出す構文が豊富なので、さまざまなフレーバーのAPIを作成できます。

🔗 進化は苦痛を伴う: アンパックすべきか、すべきでないか?

動的言語、特にRubyのようにメタプログラミングや汎用的な表現力に関連する言語で重要な概念の1つに、引数リストのアンパック(unpack)というものがあります。これは、内容が動的にのみ決定される引数リストを受け取り、そうした引数リストをメソッドに渡すものです。

アンパックの概念は、現代の静的言語の多くにも何らかの形で存在しますが、私が知る限り、アンパックの重要性はRubyに比べるとずっと低く、主に「フォーマット済みデバッグ出力」のような特殊ケース(もしくは同一型の引数の動的リスト)で使われます。

ただし動的言語では、以下のようなことがしばしば行われます。

  • 引数のいずれかが動的に収集される(続いて定義済み引数リストを持つメソッドに渡す必要があります)
  • メソッドが型の異なる引数リストを受け取ってから、統一的に処理したりミドルウェアチェイン内で次のメソッドに渡したりする
  • 何らかの汎用インターフェイスに準拠するために余分な引数を単に削除する

Rubyでは、他のいくつかの言語と同様に、splat(アンパック)引数用の構文がアスタリスク*で実装されています。

# 最も多用されるケース:「必要ならどんな引数でも'*'で受け取る」
def logged_open(log_message, *args)
  log.info("Opening the file")
  # 受け取った引数を他のメソッドに丸ごと渡す
  File.open(*args)
end

# この'*'構文はかなり柔軟性が高いので、引数リストの途中にも書ける
# of argument list:
def logged_open(path, *other_args, log_message)
  # ...
end

# 変数代入でも同様にシンメトリックな'*'構文が使える
first, *rest = list
first, *middle, last = other_list

# '*'のアンパック構文はネステッドシーケンスも扱える
def foo((left_first, *left_rest), (right_first, *right_rest))
  #
end

foo( [1, 2, 3], [4, 5, 6] )
# left_first = 1, left_rest = [2, 3]
# right_first = 4, right_rest = [5, 6]
#

しかしRubyのキーワード引数では、上とは異なる新しいアンパック機能が必要でした。つまり、すべての名前付き引数を1個の辞書に集めるか、さもなければ逆に、辞書の内容を個別の名前付き引数として渡せるようにする必要がありました。

この概念を表現するために、ダブルsplat演算子**が導入されました(おそらくPythonからの借用構文です)。

# 再び: 任意の個数の「キーワード」引数を受け取り...
def wrap_file_open(path, log_message:, **kwargs)
  log.info(log_message)
  # ...別のメソッドに渡す
  File.open(path, **kwargs)
end

# {mode: 'r'}を`kwargs`辞書にキャプチャし、
# `File.open`に渡すときに個別のキーワード引数に戻す
wrap_file_open('test.txt', log_message: "opening it!", mode: 'r')

原注

ただし新しいダブルsplat **演算子は、「古い」splat *演算子ほどシンメトリックでも強力でもありませんでした。ハッシュの一部をアンパックしてローカル変数に戻す同じ形の構文がサポートされていませんでしたし、ネストしたアンパックはできませんでした(これについてはRubyにパターンマッチングが導入されたことで変わりましたが、これは別トピックであり、Rubyではメソッド定義と関係ありません)。

そして同時に、「キーワード形式の辞書を末尾の引数として受け取る」だけのコード、つまり以下のようなキーワード引数導入前のスタイルのコード(単に古いか、意図的に古いスタイルで書かれた)は、キーワード引数の導入後もシームレスに動作し続け、下位互換性は良好でした。

def old_file_open(path, options)
  # ...
end

# 末尾の引数を引き続き暗黙で辞書扱いする
old_file_open('test.txt', mode: 'r')

Rubyは当初、この2つのスタイルの間の互換性(つまり末尾ハッシュを自動アンパックして、キーワード引数を用いるメソッドに渡す)もサポートしていました。

これは「魔法のように」動いていましたが、やがて「悪い魔法」であることがわかってきました。この「魔法のアンパック」が予想外の形で発動すると、おかしなエッジケースが多発したのです。たとえば、デフォルト値ありの位置引数(arg="default")とデフォルト値ありのキーワード引数(kwarg: "default")が混在していたり、ハッシュ形式のキー(key =>)とシンボルキー(key:)や別の種類のキーが混在していると、頻繁に混乱が生じました。

スタイルの自由な混在を許すと、コミュニティが新しいスタイルを全面的に採用するインセンティブが減退してしまいます。実際、スタイルの異なる2人の開発者が書いたコードが入り混じったコードベースがたくさんありました。


この呪われた悪循環は、(いわゆる位置引数とキーワード引数の完全な分離によって)Ruby 3.0で初めて打ち破られました。これにより、ハッシュを自動アンパックしてキーワード引数にする(あるいは逆に、キーワード引数を再パックして辞書にする)機能は基本的に廃止されました。

東京オリンピックCOVIDの年となった2020年は、Rubyにキーワード引数という概念が導入されてから7年目にして、メジャーバージョンが2から3に変わった年でもありました。2つのスタイルが入り混じったコード量がこの長い期間に増加していたため、少なくともRuby 3.0の1つ以上前のバージョンから始まったいくつもの非推奨警告に対応するための、長くつらい移行作業が必要となりました(なお、残ったエッジケースのいくつかはRuby 3.1〜3.3で引き続き解決されました。うち少なくとも1つは#20218でごく最近次期バージョンのRubyで解決されましたが、この解決方法のロジックについては未だ疑問視されています)。

名前付き引数を実現する2種類の方法(「辞書のシンタックスシュガー」と「本物のキーワード引数」)は、両者が混在しない限りこれまで通りに機能します。

また、**nilという新しい構成体もできました。これはやや不格好ですが、波かっこ{}なしのハッシュをメソッドに渡すことを禁止する形で、メソッドがキーワード引数を一切受け取らないことを明示的に示せるので、混乱を防げます。

最後に、Rubyでは「すべてを次のメソッドに丸投げする」(ミドルウェア、実装への以上、メタプロを用いるラッパーメソッド)表現が多用されていることを考慮して、「種類を問わずにすべての引数を丸投げする」ことを示す新しい記法(...)が登場しました。

def my_open(...)
  # ここに何らかの追加コードがある
  File.open(...)
end

なお、この(...)は受け取ったものを丸投げすることしかできないようになっており、受け取ったものに名前を付けることはできません。

🔗 これでおしまいですか?

私たちは、言語の設計空間の隙間にある非常に狭い道の1つをたどったにすぎません。ここで検討したのは、メソッド定義の引数部分と、名前付き(ラベル付き)引数を適切にサポートし、Ruby言語の柔軟性と利便性を維持することだけです。

本記事で説明した変更と決定の長い道のりは、何年にも渡ってRubyのメジャーバージョンをいくつもまたがっています(Rubyの小数以下バージョンは、Ruby言語の進化の大きなステップとなることが多いことを考えたうえで、あえてメジャーバージョンと書いています)。

それなりの犠牲を払い、新構文の調整ミーティングのたびに「理由もなく言語が複雑になる」だの「無意味なシンタックスシュガーだ」といった批判も相次ぎ、コミュニティで若干の緊張も引き起こされました。

これが「良い変更」だったか「悪い変更」だったか(あるいは「重大な変更」か「ちっぽけな変更」か、あるいは「正当な変更」か「不要な変更」か)といった話は、私にとって重要ではありません。
そうではなく、私は、言語で可能な設計空間の形がどのようにして探られてきたのかという(技術面と人間的な面の両方における)進化のストーリーを、「Ruby言語の当初の意図」と「現実のニーズ」と「ユーザーの習慣」という視点から、皆さんにお伝えしようとしているのです。


最後に、いくつかのサイドストーリーについても簡単に触れておきたいと思います。道端に打ち捨てられ、今も多くの人を悩ませている、大小さまざまなサイドストーリーについて。

中でも最も明白なのは、型宣言の構文をどうするかというトピックです。
Ruby言語に型を導入することについての議論や可能性の話は非常に込み入っています。私が数年前に書いたこの記事では、Rubyの構文は既に非常に豊かで、かつメタプログラミングが(メソッドの柔軟性に依存する形で)普遍的に使われているので、見た目のよい型構文をメソッド定義に追加する方法を見出すのは非常に困難であることを説明しようとしました。

メソッド定義の夢と議論に関する次のトピックは、パターンマッチングです。適切な構造的パターンマッチングが導入されたのは、上述の「キーワードの大分離」が発生したのと同じ時期(Ruby 3.0の頃)でしたが、この構造的パターンマッチングの構文は別個のステートメントのままであり、既存の「古いスタイル」のデコンストラクタにうまく溶け込ませる方法は未だに発明されていません。古いスタイルはあまり強力ではありませんが、その分言語に深く統合されています(メソッド引数のデコンストラクションなど)。これについてはRubyの型付け記事の末尾でも簡単に触れています。

メソッド定義に関連するいくつかのアイデアは、Rubyのbugtrackerで時おり議論されています。

そうしたアイデアの1つに、def foo(@a, @b)という構文を許可することで「メソッドのローカル変数への束縛を許すことに加えて、オブジェクトのインスタンス変数(Rubyでは@をプレフィックスします)への束縛も許す」というものがあります。このアイデアが最も有用な場所はオブジェクトのコンストラクタなので、真に汎用的ではありません。このアイデアが却下され続けているのはそのせいかもしれません。

もう1つのアイデアは、引数名と異なる名前のローカル変数に引数値を束縛可能にするというものです。

# 以下のように呼び出せるメソッドがあるとする
send_event(on: :monday, if: -> {... 何らかの条件 ...})

# このメソッドは以下のように定義されているだろう
def send_event(on:, if: nil)
  # メソッド内では`on`ローカル変数を使う必要がある
  if Date.today.wday == on # ...が、ここではメソッド呼び出しの場合ほど明確ではない
    # ...

  # `if`はキーワードなので`if`という変数は使えない
  # つまり以下のように冗長な書き方をするしかない
  condition = binding.local_variable_get('if')
  # ...
end

# では以下のように書けるとしたらどうだろう?(これは有効なRubyではない!)
def send_event(on wday:, if condition:)
  # これなら`on:`引数の値に`wday`ローカル変数が利用可能になり、
  # `if`引数の値も`condition`として利用可能になる

たとえばSwiftにはそうした機能があります
Rubyでは#16460#18402などで何度か議論はされていますが、(まだ)結論は出ていません。

次回予告

今回のテキストで扱ったのは、Ruby言語設計のごく一部、すなわちメソッド定義の引数部分にとどまりますが、それでも、Ruby言語要素の進化を記述する方法論を確立し、今後の章で拡張されるいくつかの基本的な言語の特性を定義するのに役立ちました。

メソッド定義の他の要素とみなせるもの(可視性、所有権、装飾、既存メソッドの再定義など)については、次回の「メソッドが定義されると何が起きるのか?」(仮題)で解説します。

どうぞご期待ください。


お読みいただきありがとうございます。ウクライナへの軍事および人道支援のための寄付およびロビー活動による支援をお願いいたします。このリンクから、総合的な情報源および寄付を受け付けている国や民間基金への多数のリンクを参照いただけます。

すべてに参加するお時間が取れない場合は、Come Back Aliveへの寄付が常に良い選択となります。

本記事(あるいは過去の私の仕事)が有用だと思えたら、Buy Me A Coffeeサイトにある私のアカウントまでお心づけをお願いします。戦争が終わるまでの間、ここへのお支払いは(可能な場合)私や私の戦友たちが必要とする装備または上述のいずれかの基金に100%充てられます。

関連記事

Ruby: "uselessシンタックスシュガー"シリーズ「キーワード引数やハッシュの値省略」(翻訳)

Rubyの型アノテーションの現状についていくつか思うこと(翻訳)


  1. 訳注: この名称はGold of Polubotokという半ば伝説的なストーリーになぞらえているようです。 
  2. obj.fooのように、識別子にレシーバーが明示的に付けられている場合、このfooは間違いなくメソッド名です。しかし、レシーバーも引数もないむき出しのfooという識別子は、メソッド名かもしれませんし、現在のスコープ内に存在するローカル変数名かもしれないのです(なお、メソッドとローカル変数が両方存在する場合はローカル変数が「勝ちます」)。この仕様は混乱を招きそうに思えるかもしれませんが、実際にはコード設計上の便利なツールなのです。ある値の名前がメソッドである場合に、それを現在のメソッドの引数として扱ったり、ローカルで算出された変数として扱ったりすることが、コードを書き換えずに行えるからです。この手法は「ベアワード(bareword)」と呼ばれ、有名なRubyistであり教師であるAvdi Grimmが導入した用語です。 
  3. Rubyistの皆さんなら、きっとHash#fetchを使うところでしょう(必須キーとデフォルト値付きオプショナル引数を両方指定するために)。しかし本記事ではより広い読者層にとって読みやすくなることを目指しているので、Hash#fetchを使うかどうかにかかわらず、私が話している一般原則の部分は変わりません。 
  4. 訳注: 他の言語では「名前付き引数(named arguments )」などと呼ばれることがよくあります。 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。