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

Ruby: "uselessシンタックスシュガー"シリーズ「引数のforwarding」(翻訳)

概要

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

日本語タイトルは内容に即したものにしました。段落編成を若干変更し、訳文の一部に強調を加えています。

forwarding(転送)は本記事では英ママとしました。anonymousは「無名」としました。

参考: プロと読み解くRuby 2.7 NEWS - クックパッド開発者ブログ

Ruby: "uselessシンタックスシュガー"シリーズ「引数のforwarding」(翻訳)

本記事は、最近のRubyで出現した"無用な"(さもなければ物議を醸す)構文要素を扱うシリーズ記事の一環です。本シリーズの目的は、そうした機能を擁護することでも批判することでもなく、その機能が導入された理由、設計、そして新構文を使うコードに与える影響を分析するための一種の「思考のフレームワーク」を皆さんと共有することです。本シリーズのあらまし記事と目次もご覧ください。

今回取り上げる引数のforwardingのさまざまなショートカットは、これまでのシリーズ記事と異なり、珍しくほとんど反対の声が上がらなかった機能です。

🔗 引数のforwardingとは何か

Ruby 2.7から、以下の書き方が使えるようになりました(注: この...はコード内で完全に有効な構文であり、ブログ記事でよく使われる「以下省略」の意味ではありません)。

def foo(...)
  # barは、fooが受け取った「位置引数」「名前指定引数」「ブロック引数」を
  # すべて受け取る
  bar(...)
end

Ruby 3.1からは、以下のように&で無名ブロック引数だけを単独で"forward"できるようになりました。

def iterate_through_data(&)
  data.each(&)
end

Ruby 3.2からは、以下のように位置引数(positional arguments)と名前指定引数(キーワード引数)も***でそれぞれ単独で"forward"できるようになりました。

def split_arguments(*, **)
  pass_positional(*) # *はすべての位置引数を渡す
  pass_keywords(**)  # **はすべてのキーワード引数を渡す
end

split_arguments(1, 2, 3, a: 4, b: 5)
# この呼び出し後、これらのメソッドは以下の引数で呼び出される
#   pass_positional(1, 2, 3)
#   pass_keywords(a: 4, b: 5)

🔗 引数forwarding導入の理由と経緯

今回は、理由と経緯を切り離せません!

...の導入についての議論には長い時間がかかったわけではありません(少なくとも私はそうした議論を見かけていません)。むしろ、Ruby 2.7における大きな変更というビッグウェーブに乗って即興で考案されたのです。

Ruby 3.0に備えた最後のバージョンであるRuby 2.7には「大きな変更」がいくつも導入されたので、変更に備える時間を誰もが確保できました。そうした大きな変更の1つに、位置引数とキーワード引数の最終的な分離があり、引数を扱うルールのいくつかを厳密にすることで長年に渡る煩わしさを軽減しました。

このトピックは複雑ですが、Ruby公式の解説で十分詳しく述べられています。

引数forwardingの省略記法が誕生した背景には、引数がこのように分離された結果、すべての引数を丸ごと別のメソッドに渡すだけの単純なメソッドを書こうとすると、複雑な書き方になってしまうという副作用が生じていたことを押さえておく必要があります。

Ruby 2.7より前は以下のようになっていました。

def reader(name, mode:)
  # 何か読み出す
end

def writer(name, content)
  # 何か書き込む
end

def wrap(method, *args)
  # 典型的なラッパーメソッド
  # ログ出力、他のエラーのキャッチといった
  # より複雑なディスパッチ作業などを行う
  send(method, *args)
end

wrap(:reader, "file.txt", mode: 'wb')
wrap(:writer, "file.txt", 'some content')

上は、メソッドシグネチャ内とメソッド呼び出し内の*argsがあれば、すべての位置引数とキーワード引数を一括で受け渡しするのに十分でした。

Ruby 2.7になると、位置引数とキーワード引数の分離の形式化が進んだので1、メソッドは常に以下の形式で書かなければならなくなりました。

def wrap(method, *args, **kwargs)
  send(method, *args, **kwargs)
end

一般的な状況では、第3の引数であるブロック(Rubyの特殊な「末尾lambda」)についても考慮されるべきだったでしょう。すべての引数をもれなく渡せる、真に普遍的な委譲を行うには、以下のように書く必要がありました。

def wrap(method, *args, **kwargs, &block)
  send(method, *args, **kwargs, &block)
end

やりたいことは本当にシンプルなのに、そのためにこれほど多くの構文と命名が必要だったのです!

そういうわけで、実用的なソリューションとして「すべての引数を丸ごと渡すだけ」のショートカットが#16253で提案され、...という形で承認されたのです。

いくつかの技術項目やエッジケースについても議論されましたが、一部はすぐに解決され、一部は次のバージョンで解決されました。たとえば、...の前に引数を書けるようにする機能はRuby 3.0で導入されましたが、その有用さが認められて2.7ブランチにもバックポートされたので、Ruby 2.7.3あたりから利用可能になりました。

「すべてを丸投げする」表現はRubyで非常に広範囲に使われており、あらゆる効果に用いられているので、かなり大きな成果を得られました。

# 実際の呼び出し順の複雑さを隠蔽する
def log(text, level:, **payload)
  Loggers::Registry.fetch(:http_logger).log(text, level: level, **payload)
end

# 実装を動的に選ぶ
def make_event(type, sender, **details)
  EVENT_CLASSES[type].new(sender, **details)
end

# メソッドを動的に定義するいい感じのDSLを作る
class HTML
  def method_missing(name, content, **attributes)
    # タグのname・content・attributesからタグ文字列を構築する
  end
end

HTML.new.a('Ruby', href: 'https://ruby-lang.org')
#=> "<a href='https://ruby-lang.org'>Ruby</a>"

上述のケースは、どれも...を使えばうんと短く書けるようになります。しかも「単にすべてを渡す」という本当の意味も失われません!

この変更は、見解の異なる多くの派閥がただちにメリットを得られる珍しいケースだったのです。

  • 構文の変更や短縮記法に関心のある人々は、この変更の便利さに気づいた
  • 「ユースケースは?」と聞き返すことの多い慎重派も、多くのユースケースがあることにすぐさま気づいた
    (特にRuby 2.7以降は、引数の委譲を...*args, **kwargsのどちらかに書き換える必要に迫られていたので、ショートカットに落とし込む方法は受け入れやすそうに思えた)
  • 最後に、言語を実用的なエンジニアリングツールとして重視する人々は、パフォーマンス向上を実感した

パフォーマンス向上についてはシンプルに説明できます。名前を持たないものは、オブジェクトに入れる必要が生じないからです。たとえば、以下の書き方は、位置引数とキーワード引数を保存するために中間の配列とハッシュをアロケーションします。

def delegator(*args, **kwargs)
  # この`args`はArray、`kwargs`はHash
  # ...しかし値をただちに取り出すためだけに必要になってしまう
  delegatee(*args, **kwargs)
end

以下のコードでは、利用可能なローカル変数を増やさないので、配列やハッシュをアロケーションする必要は生じません。

def delegator(...)
  # ここには他にローカル変数はない
  delegatee(...)
end

そういうわけで、その後いくつかのバージョンで「位置引数だけを渡すショートカット」や「キーワード引数だけを渡すショートカット」の分離が提案されたときも、動揺やスキャンダルは引き起こされませんでした。

しかも小さな変化で済みました。以下のシグネチャはいずれも有効な構文です。

def ignore_my_args(*)
end

def ignore_keyword_args(some, positional, **)
end

これは「このメソッドは、位置引数またはキーワード引数を任意の個数受け取れる(おそらく周辺クラスの同名メソッドとの互換性のため)が、それらを使わずに無視する」という意味でした。そこで、Ruby 3.2では「...、そしてそれらをさらに別の場所に渡す」という意味をその後ろに付け加えるだけで済んだのです。

def pass_my_args(*)
  other_method(*)
end

def pass_keyword_args(some, positional, **)
  other_method(**)
end

「新しい構文を必要としない」という直感的な感覚、そして...という記法が既に存在していたこと、そして「余分なアロケーションを必要としない」という同じ議論が考慮されて、この変更はただちに受理されました。#18351でこの提案を行った(しかも高品質の実装も提供した)のが、SequelやRodaなどの有名ライブラリの作者であり、RubyコアメンバーであるJeremy Evansだったことも追い風となりました。

この新しい構文の実に興味深い点は、別メソッドへの委譲に使えるだけでなく、名前付き変数からの値の取り出しが行われるほぼあらゆる場所でもサポートされていることです。

def with_anonymous_args(*, **)
  ary = [*]
  hash = {**}
  p ary, hash
end

with_anonymous_args(1, 2, 3, a: 4, b: 5)
# Prints:
#   [1, 2, 3]
#   {:a=>4, :b=>5}

私の単なる好奇心とも言えますが、この構文は少なくとも「すべてを丸投げする」メソッド内で一時的なデバッグ用ステートメントとしても有用な可能性があります(ただし少なくとも配列やハッシュをインスタンス化しないというメリットは失われますが)。

def make_event(type, **)
  puts "DEBUG!" if {**}.key?(:password)  # temp
  EVENT_CLASSES[type].new(**)
end

あるいは、「パターンマッチング記事パート3の"この先どうなるか"セクション」でお目にかけた少々大げさな例のように、「名前を気にしない」アイデアを再利用して引数リストの一部をさまざまな意味(つまり名前)で表すシグネチャとして引数リストのパターンマッチを改良できるでしょう。


無名ブロックのforwardingを表す&については、もう少し話が複雑になります。

*や **の場合と異なり、&には「このブロックを単に無視する」という意味はありませんでした。Rubyのブロック引数はどんなメソッドにも渡そうと思えば渡すことが可能になっているため、メソッドシグネチャでブロック渡しを必須にする方法も、ブロック渡しを禁止する方法もありません(ブロック受け取りをまったく想定していないメソッドに誤ってブロックが渡されると(そして無視されると)問題になることがありますが、これは解決が非常に難しい問題です: #19979#15554)。

そういうわけで、Ruby 2.7の「引数forwarding」が使えるようになるずっと前に、#11256で単独の&をブロックのforwardingに使おうと提案されたとき、主にブロックのアロケーションの最適化について最新の注意が払われました。当時、その最適化部分については、ブロックが名前付きの場合にもブロックの受け渡しを最適化する目的のためだけに、#14045で独自に実装されました。

しかしその後、...による基本的な引数forwardingがRubyに既に取り入れられると、「&はRubyで受け入れ可能か」「&の読みやすさはどうか」という6年にわたる議論は終結し、Ruby 3.1で導入されたのです。

これは、キーワード引数の値省略で見たときと同じ効果です(ハッシュの値省略key:構文の別のケースとして使われるようになった後、キーワード引数の値省略も言語で受け入れられるようになってきました)。「より大きな機能」が注目されると、「より小さな機能」でもそうなりやすくなる可能性があるのです。

🔗 引数forwardingのむずかゆい点

...ベースの委譲記法でうっすらむずかゆい点は、丸かっこ()に関連するものです。この問題の本質は、以前キーワード引数の値省略で見たものと似ています。

def my_method(...)
  p ...
end

上は、実際には以下のように解釈されます。

def my_meth(...)
  (p()...) # 引数が空のメソッド呼び出しと、endレスrange`...`
end

この問題が影響するのは...だけです(他の無名forwarding記法には影響しません)。以下のように()を追加するだけで普通に修正できます。

def my_method(...)
  p(...)
end

さらに困る点は、無名forwardingはブロックやprocではサポートされていないことです。古い無名splat *はブロックやprocでサポートされていることを考えると、これは特に混乱を招く点であり、以下のような非常に混乱するコードを書いてしまう可能性があります。

def process(*)
  # ...たくさんのコード...
  ['test', 'me'].each { |*| puts(*) }
end

process('input')

上のコードは"test"と"me"を出力する(proc内部の*がその引数を受け取ってputsに渡す)ように見えますが、実際には以下が出力されます。

input
input

ここで起こっていることは次の通りです。

  • each { |*|は、「全ての引数を受け取って握りつぶす」という古いロジックとして扱われる。
  • puts(*)は「そのコンテキストに無名forwarding引数があるかどうかをチェックする」 という新しいロジックとして扱われ、メソッドの引数も同様に扱われる。

これについては#19370で議論が続いており、(少なくとも私には)混乱して見えます。Ruby開発者のミーティングでは、このようなケース(*引数を持つブロック内での*によるforwarding)を禁止し、より明確なケースについては許可するという方向に傾いているようです(訳注: その後#9330の修正がマージされて#19370はcloseしました)。

🔗 引数forwardingの導入後どうなったか

引数のforwardingショートカットを(慎重に)使うようになると、明示性の兆しが現れます。

ここでいきなり明示性(explicitness)という言葉を持ち出すと混乱するかもしれません。明示性は、コードに名前を追加したり値をビルドする手順を追加したりすることでコードを明確にすることを指すことが多いからです。たとえば、数式をいくつかの名前付きローカル変数に分割することや、あるメソッドの結果を別のメソッドに渡す代わりに、最初に何らかの名前をつけることなどがそうです(病的なケースになると「重要な呼び出し/チェック/計算は、常にその利用法を説明する独自のメソッドとすべき」というのもありますが)。

しかしここで私が話しているのは、コードのある程度の範囲、つまりコードの「ページ」や「章」に相当する部分について、その意図を明示できるようになることなのです(シリーズのあらまし記事で強調したのと同様に、「ある1行だけを取り出して読みやすいかどうかを云々する」のではなく、「コードを読む人がストーリーの大枠を気持ちよく理解できるかどうか」について話しているのです)。

以下のようなコードがあるとします。

def event(type, sender:, content:, details:)
  EventBus::Registry.instance.push_event(type, sender: sender, content: content, details: details)
end

この種の「中間管理職」的なメソッドは、階層化システムでよく見かけられます。そうしたメソッドのパラメータは既にチェック済みで、デフォルト値は上位のレイヤから与えられ、処理そのものはもっと下のレイヤで行われます。そして、この現在のメソッドは、現在のモジュールのショートカットに過ぎません(おそらく何度も呼び出されるので、下位レイヤの冗長な呼び出しを何度も繰り返すのは面倒です)。

この定義を短くする方法はいろいろあります。
たとえば値省略が使えます。

def event(type, sender:, content:, details:)
  EventBus::Registry.instance.push_event(type, sender:, content:, details:)
end

キーワードのsplat演算子**も使えます。

def event(type, **event_data)
  EventBus::Registry.instance.push_event(type, **event_data)
end

しかしこのメソッドの本来の目的は、「パラメータを別の場所に丸投げする」ことだけであり、この**event_dataという引数名にコードの作者が込めた気持ちは「細かいことは知らんけど、とにかく何が来ようとエイヤで丸投げするだけだからね」に近いものです(この種の引数は、残りの引数を意味する**kwrestや、**optionsなどと呼ばれることがよくあります: 後者の**optionsは、末尾のハッシュ引数やキーワード引数に伝統的に付けられることが多かった名前で、長年に渡って誤解の元となりました)。

つまり最終的に、以下のように書くことで、そうした「もうわかってるよ」「丸投げするよ」というお気持ちを露わにすることが可能になるのです。

def event(...)
  EventBus::Registry.instance.push_event(...)
end

これはメソッドの1行定義(これは次回の記事で取り上げます)でも同様に書けます。

def event(...) = EventBus::Registry.instance.push_event(...)

このようなシンプルな記法は、設計のあらゆる段階で使われるようになる可能性があります。

たとえば、初期のプロトタイプでは「単にすべてをパススルーする」ことで、レイヤを合理的な形で簡単に積み重ねられるようにしておいて、その後で個別のステップでパススルー以外の責務を順次明確にする、ということが考えられます。

また、設計と仕様を長期間かけて明確化した後で、レイヤ同士の機能や期待をすり合わせるときに、細かい部分は文字通り...で省略してとりあえず動くようにしておくと、それなりに有用であることが判明する場合もあるでしょう。この作業を意識的に行えば、シグネチャが不当に変更されたことがレイヤを通じて明らかになり、結果として有用なクリーンアップにつながるかもしれません。

いずれにしろ、「ここは重要な部分じゃないよ」「ここはわかりきった部分だよ」ということをはっきり示せる機能があれば、コードを読む人が他の重要な部分に集中できるようになります。

あるいはこう言い換えてもよいでしょう。「どれもこれも重要である」とすることは(たとえば長い説明的な名前に全部カラーマーカーを引いて強調するように)、どれもこれも重要ではないのと同じことになってしまうのです。

これこそが、私が明示性について語っている理由です。ナンパラのときも似たようなことがありましたが、値に名前がついていても、それは何かを説明しているそぶりを見せているだけで大して意味がない場合もあるのです。そんなときは、「ここではこれ以上説明することは何もない」という意図をくっきりと明示的に示せる構文が有用です。

🔗 ウクライナ通信

ほんの少しお時間をください。今の私たちが暮らしている状況を小さな思い出として記事の途中にはさむことにしています。私は現在戦争中の国で暮らしています。先週起きたささやかな出来事を無作為に選んでお伝えするものです。

数日前は、とある出来事の「記念日」でした。私の地元で、全面侵攻以来3000回目の空襲警報が鳴らされました(警報が鳴った時間を合算すると既に100日を超えています)。

引き続き記事をどうぞ。

🔗 他の言語ではどうやっているか

前回の値省略構文では、「すべての引数を丸ごと渡す」構文と正確に対応するものを他の言語で見つけようとして相当苦しみました。理由の一部は、(*args, **kwargs, &block)に相当するものが他の言語にないせいかもしれません。これは、「関数に渡されたものすべて」を規範的に、かつ最も短く表現する方法です。

今回の引数forwardingにおける設計の選択の余地は、可変長引数、つまり関数に渡せる引数の個数が可変であることに関連しています。この概念は、しばしばvariadic argumentsやvarargsと呼ばれ、printfなどの書式付きprint関数を用いて説明されることもよくあります。

可変長引数が使える言語は、いくつかのグループに分けられます(なお、Rustのように可変長引数をあえて取り入れていない言語や、Zigのように初期の段階で可変長引数を採用した後に廃止を決めた言語もあります)。

🔗 グループ1: 旧勢力

「受け取れる変数の個数は可変ですよ」という宣言を関数シグネチャの中で行うタイプで、アクセスするために(変数や関数やマクロで)何らかの特殊な名前を指定します。たとえばC言語では以下のように...を使います。

int sum(int count, ...) {
  va_list args;
  va_start(args, count);
  // `args`の`count`を処理するたびに`va_arg(args, int);`を呼び出す
  va_end(args);
}

🔗 グループ2: 旧勢力(ただし動的言語)

古いJavaScriptや古いPHPの関数は、シグネチャに関わらず可変長引数を受け取り2、それらをarguments(JavaScript)またはfunc_get_args()(PHP)で公開可能でした。

function variadic() { console.log(arguments) }

variadic(1, 2, 3, {foo: 'bar'})
// 以下を出力する
//   Arguments(4) [1, 2, 3, {foo: 'bar'}]

このグループのラディカルな変種であるPerlのsubには、そもそも引数を宣言する構文がまったくありません3。サブルーチン内ではリスト変数@_ですべての引数にアクセス可能であり、引数に名前を付ける方法は引数をローカル変数に代入することです。

sub test {
  my($x, $y) = @_;
  print "x=$x y=$y\n"
}

test(1, 2)
# prints x=1, y=2

🔗 グループ3: 新興勢力!

多くの言語には、パラメータのリストを「キャッチ」する名前付きパラメータを関数シグネチャで宣言する特殊な構文があります。
最新のJavaScriptでは以下のようになります。

function new_variadic(...myargs) { console.log(myargs) }

new_variadic(1, 2, 3, {foo: 'bar'})
// 以下を出力する
//   [1, 2, 3, {foo: 'bar'}] --特殊な"Arguments"オブジェクトラッパーがない点に注意

指定に使われる記法は多くの場合...または*ですが、C#ではparamキーワードが、Kotlinではvarargがそれぞれ使われています。

現在では、これらが慣行として一般に合意されているようです。

それと対になる方法についても、多少の差はあれ合意されているようです4。値のリスト/配列/タプルがあって、それらを分けて関数に渡したいときは、それ用の演算子を利用できます(splat演算子やspread演算子と呼ばれることが多いそうした演算子は、「残りの引数」宣言と外見が同じなのが普通です)。

function function_with_3_args(arg1, arg2, arg3) {
  console.log({arg1, arg2, arg3})
}
args_in_array = [1, 2, 3]
function_with_3_args(...args_in_array)
// 出力: {arg1: 1, arg2: 2, arg3: 3}

そうした演算子は、パススルー(ある関数が「渡された引数は何でも」受け取って、別の関数に渡すこと)を実行する方法を提供します。

しかし昔は当たり前ではありませんでした!argumentsが使われていた時代のJavaScriptでは、「すべての引数をさらに別の場所に丸投げする」のは以下のように面倒だったのです。

function b(){
    console.log(arguments); //arguments[0] = 1など
}
function a(){
    b.apply(null, arguments); // 受け取ったものをパススルーする(最初のnullは`this`用)
}
a(1,2,3);

つまり、現代では(通常は、下のレイヤで「分割」される名前付きsplat変数が1個だけある場合)、ほとんどの状況で十分パススルーできるということです。

しかし待ってください!

Luaでは興味深いことが起こりつつあります(この記事を書いているときに気づきました)。

Luaはバージョン5.0以来「グループ1」に属していました。つまり、シグネチャに...を書くことで「引数を任意の個数受け取れる」ことを表し、関数内では特殊変数argで引数のテーブルにアクセスできます。

function f(...)
  print(arg)
end

f(1, 2, 3)
-- prints  1  2  3

そしてLua 5.1では、特殊変数argが ...置き換わったため、Rubyのショートカットに最も近いように見えます。

function f(...)
  print(...)
end

f(1, 2, 3)
-- 出力:  1  2  3

-- ...はシンプルな変数としても利用可能
function f(...)
  print(...+5)
end

f(1) -- 出力: 6

プログラミング言語の進化は、驚きに満ちた世界ですね!


余談: ...を、「以下同文」という意味に沿った構文構造として使いたくなる誘惑にかられているのは、RubyやLuaだけではありません。少なくともPythonでは、生の...を書くとEllipsisオブジェクトが生成され、それを通常の値として受け渡しできるようになります。しかもこれには多くの用途があるのです。

def method():
  ... # 何もしないメソッドであることを表す

# funの型を「任意の入力引数を渡して呼び出し可能で、結果をstrで返す」ものとして指定する
fun: Callable[..., str]

matrix = np.matrix([[1, 2], [3, 4]])
# すべてのデータをあらゆる次元で受け取る(0番目のカラムは除く)
matrix[..., 0] #=> matrix([[1], [3]])

🔗 この先どうなるか

実装される見込みは薄いのですが、未だに惹きつけられるアイデアが1つあります。「あ、ここはわかりきってるからいいよ」と言える場所をときどき変更することは可能でしょうか?

次のようなコードを考えてみましょう。いわゆる「callableなクラス」(多くのprivateメソッドに分かれたステップによる複雑なアルゴリズムがカプセル化されている可能性があります)でよく使われる定番の書き方です。

class MyOperation
  def initialize(some, arguments, of:, various: "kinds")
    # 引数を代入する
  end

  def call
    # ... 実装 ...
  end

  # publicインターフェイス:
  def self.call(...) = new(...).call
end

想定される利用方法はシンプルです。

MyOperation.call(with, some, of: :arguments)

これは、渡されたすべての引数を受け取ってインスタンスを作成し、ただちに#callメソッド(実装の核心)を呼び出します。

ここで問題なのは、このクラスの唯一のpublicメソッドがシグネチャで何の情報も表していないことです。これではAPIドキュメントを自動生成することも、イントロスペクションで抽出することもできません。

m = MyOperation.method(:call)
#=> #<Method: MyOperation.call(...)> -- 情報が少なすぎる
m.parameters
#=> [[:rest, :*], [:keyrest, :**], [:block, :&]] -- これも情報が少なすぎる!

この問題があらわになる状況は、この「callableなラッパー」だけではありません。典型的なHTMLクライアントの実装は以下のような感じになるでしょう(最新のRubyで記述されています)。

def get(...)
  make_request(method: :get, ...)
end

しかしここでも、publicインターフェイスメソッドgetは、(make_request privateメソッドの実装が知っているはずの)シグネチャの情報を何も示してくれません。

では、明示的な引数宣言が存在する場合でも「すべてを渡す」構文を引き続きサポートすることは可能でしょうか?以下は「可能な限り短く表す」ことと「初心者向けの練習問題並みにすべてを記述する」ことの折衷案です。

def get(endpoint, params: {}, headers: {}, redirect: false) # ここにはみっちり書く
  make_request(method: :get, ...) # ここでは「単にそれをさらに渡す」だけ
end

このアイデアについて述べられているチケットが#16296にありますが、さほど注目されませんでした。

ところで、Rubyにはこれがうまくいく場所があるのです!
super(このメソッドの親クラスバージョン)を呼び出すと、暗黙で現在のメソッドのすべての引数が渡され5、そのおかげで非常に優秀なショートカットをいくつか使えるようになります。

たとえば、Dataの初期化でデフォルト値をいくつか追加する必要が生じたとしましょう。

class Measurement < Data.define(:amount, :unit)
  def initialize(amount:, unit: '-none-') = super
end

Measurement.new(100, 'm') #=> #<data Measurement amount=100, unit="m">
Measurement.new(100)      #=> #<data Measurement amount=100, unit="-none-">

すなわち、「受け取るパラメータは明示的に宣言し、それらをすべて渡す」というアイデアは、少なくとも想像もつかないということはありません!

🔗 まとめ

今回のまとめは以下のような感じです。

  1. あらゆるものに名前を付けなければならないとは限らない
    これについてはナンパラの回でも既に説明しました。

  2. 「そこに他の意味が付け加えられていない」ことを明示的に示す
    これは、本当に重要な部分を強調するうえで考慮すべき、有用な明示性です。

  3. パフォーマンスの最適化と明瞭さの最適化が、まるで惑星直列のように奇跡的に調和することがある
    ここから生み出される機能は、コードを複数の観点から同時に説明し、保護する可能性があるでしょう(まだこの機能を好まない人もいますが)。

次回は、エイプリルフールのジョークから生まれた構文機能である(endレス)1行メソッド定義を取り上げます。

今後の記事をフォローしたい方は、Substack に登録いただくか、Twitterでフォローをお願いします。


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

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

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

関連記事

Ruby: "uselessシンタックスシュガー"シリーズ記事のあらましと予告(翻訳)


  1. 繰り返しになりますが、詳しい内容については公式の解説ですべて説明が尽くされています。ここでは、どんなタイミングでどこに噛み付いてくるかわからない、混乱を呼ぶ多くのエッジケースを解決するには、位置引数とキーワード引数の分離がどうしても必要だったことを知っていただければ十分です。 
  2. この書き方は現在も可能ですが、ほとんどの場合古い書き方を嫌って新しい書き方が支持されています。 
  3. chrismorganHacker Newsで指摘してくれました。Perl 5.20.0(2014年5月)から引数を宣言可能になっており、Perl 5.36.0(2022年5月)から安定したとみなされています。 
  4. これは少なくとも動的言語での話です。静的言語における状況やその課題は、さまざまなニュアンスを含んだ興味深い設計の余地を多く生み出しており、別記事のトピックになる可能性もあります。 
  5. 実際は、ローカル変数の現在のすべての値に、現在のメソッドの引数名を付けたものになります。これは興味深い結果をいくつかもたらしますが、これに夢中になるのはやめておくことにします。 

CONTACT

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