Crystal言語作者がRubyを愛する理由(4)呼び出しが強力(翻訳)
完璧なAPIを設計する
前回の記事では、Rubyのメソッド呼び出しがいかに多用されているか、またさまざまな方法で使われているかについて書きました。Rubyのユーザーが普段からメソッド呼び出しを多用するのであれば、メソッド呼び出しは使いやすいに越したことはありません。
Rubyのメソッド呼び出しは極めて柔軟かつ強力であることがわかります。いくつか例をご紹介しましょう。
# ホワイトスペースで文字列を区切る
# 引数なしの場合
"a b c".split # => ["a", "b", "c"]
# 引数に文字列を1つ渡す場合
"a,b,c".split(",") # => ["a", "b", "c"]
# 引数に文字列と上限値を渡す場合
"a,b,c".split(",", 2) # => ["a", "b,c"]
# ブロックを渡す場合
"a,b,c".split(",") do |piece|
puts piece # "a", "b", "c"の順に出力される
end
# ブロックと上限値を渡す場合
"a,b,c".split(",", 2) do |piece|
puts piece # "a"に続いて"b,c"が出力される
end
# 正規表現を渡す場合
"a,,,b,,c".split(/,+/) # => ["a", "b", "c"]
# 正規表現と上限値を渡す場合
"a,,,b,,c".split(/,+/, 2) # => ["a", "b,,c"]
"a\nb\n".each_line do |line|
p line # "a\n"に続いて"b\n"が出力される
end
"a\nb\n".each_line(chomp: true) do |line|
p line # "a"に続いて"b"が出力される
end
File.join("usr", "mail") # => "usr/mail"
File.join("usr", "mail", "gumby") # => "usr/mail/gumby"
ここではString#split
、String#each_line
、File.join
という3つのメソッドしか使っていませんが、この例だけでもメソッドでできることについてかなり見当が付きます。
- 引数の個数が変わると振る舞いが変わることがある。この場合、
split
に引数を渡さない場合はホワイトスペースで分割される。返される値の個数の上限も整数で指定できる。 - 引数の型が変わると振る舞いが変わることがある。
split
メソッドにはString
型だけでなくRegexp
型も渡せる。 - ブロックを渡すと振る舞いが変わることがある。この場合、
split
メソッドにブロックを渡すと、各部分がブロックにyield
される。 - 引数によっては
chomp:
のようにオプション名を付けて指定できる引数もある。この場合は、末尾に改行があれば削除する。 - メソッドによっては、渡せる引数の個数が可変になっていることもある(可変長引数)。
File.join
は、引数で渡されたものをすべて結合するので、事前に配列に入れておく必要がない。
ユーザー目線で言えば、文字列を分割したければsplit
メソッドを呼ぶだけでよいということになります。分割で区切り文字を指定したいこともあれば、正規表現を使いたいこともあるでしょう。オプションで上限値を指定することもできます。おそらく文字列を配列に取り込んでおく必要もなく、各部分文字列をブロックにyield
できればよいこともあるでしょう。どれをやりたい場合でも、split
だけですべてまかなえます。
私はこの点を心から気に入っています。理由は以下のとおりです。
- 文字列の分割を文字どおり
split
でやれること。同じ操作のために複数のメソッド名を使い分ける必要があるだろうか? - 柔軟性が高いので、APIを設計するときにユーザーがどう呼び出すかを正確に選択できる。引数に名前を付けたければそうすればよいし、オプション引数が使いたければそれも可能。つまり心に思い描いたとおりのAPIを設計できる。この言語はAPI設計にほとんど縛りをかけない。
他の言語の場合
今度は他の言語とも比較してみましょう。私がRubyのこの点を好ましく思っているということは、他の言語では必ずしもそうならないということです。本シリーズではRubyを他の言語と比較していますが、その意図は「Rubyのここが好き」と申し上げたいだけであり、他の言語を貶めるつもりは毛頭ないことをあらかじめお断りしておきます。
Javaはメソッドのオーバーロードが可能で、個数や型が異なる引数を扱えるメソッドを定義できます。さて、Javaにはデフォルトの引数がありませんが、オーバーロードを2つ定義して、一方のメソッドが他方のメソッドをデフォルト値で呼び出せばこの問題を回避できます。KotlinはJavaバイトコードにコンパイルされますが、デフォルト引数と名前付き引数についてはしっかりサポートしています。いいですね!
C#は、当初Javaと非常に似通っていましたが、その後ゆっくりと素晴らしい方向へと進化を遂げました。今ではデフォルト引数はもちろん、名前付き引数もサポートしています。これもいいですね!
Go言語にはオーバーロードも、引数のデフォルト値も、名前付き引数もありません。文字列を分割したいときは以下のようにします。
Split
: 指定の区切り文字で分割するSplitN
: 指定の区切り文字で分割し、最大N個の結果を返す
さらに、正規表現を使いたい場合はSplitRegex
やSplitRegexN
も必要になります。分割のオプションをさらに増やしたければ、それぞれの関数に追加しなければなりません。はい、大した手間ではありませんよね。しかし私はRubyのやり方の方が好きです。Rubyでは、上限値を指定したければ単にオプションを追加するだけで済みます。その点はGoも同じですが、関数名にN
を追加して変更する必要があります。どんな関数名があるのか忘れてしまうかもしれません。しかしRubyならいつもsplit
でやれるのです!
Rustはさらに新しいモダンな言語ですが、デフォルト引数も名前付き引数もありません。
お次はHaskellとElmですが、こちらはまったく仕組みが違います。HaskellもElmも、引数が必要な個数より少なくても関数を呼び出せるようになっています。はい、文字どおりそうなのです。つまり、不足している引数を期待する別の関数が生成されるということです。これは関数の部分適用(partial application)と呼ばれるものだと思います。部分適用は極めて強力かつ有用な機能です。しかしこの言語はデフォルトでこのようになっているので、以下のようなことが起きます。
- 呼び出し側が引数を渡し忘れても、エラーメッセージがまったく表示されない。呼び出した結果の値を後で使おうとしたときに初めてエラーメッセージが表示される。このため、問題を特定するのが非常に難しくなる。
- デフォルト引数が使えない。引数を渡さなかった場合はデフォルト値を使うべきなのか、それとも部分適用と解釈すべきなのかを決定できない。
- 引数の個数に応じたオーバーロードはできない(これも今と同じような理由)。
実際には、このような場合の自由度が低いために、関数名をどうするかも考えておかなければなりません。Haskellでは、引数の個数や型が違う関数名に'
や ''
を付けているものがあることを私は知っています。これではコードがわかりにくく覚えにくくなるだろうと私には思えます。
デフォルト引数がない言語では、何とかしてデフォルト引数をシミュレーションする方法を個別にひねり出すことになるでしょう。つまり、定型文がいくつも出現し、各自が思い思いの方法で実現する可能性が生じるため、コードの統一性が低下してしまうでしょう。
Rubyのメソッドについてあと一言
それだけではありません。Rubyのメソッド名には制約がありません。たとえば、Javaではvoid
という名前の関数は使えません。void
はキーワードだからです。Goではtype
という名前の関数は持てません。これもキーワードだからです。最近、Elmではtype
という名前が使えないことに気づきました。私が間違っていなければ、そうした名前を問題なく使えるようにする方法はあるはずなのですが。
Rubyならこんなことができます。
class Foo
def def
1
end
end
Foo.new.def # => 1
Rubyではbegin
、end
、class
などどんな名前でも使えます。これらを呼び出すにはドット.
が必須なので、曖昧にはなりません。実を言うと、レシーバー(ドット.
の前にあるもの)がないメソッドでも、たとえばself.def
のように書けば問題なく呼び出せます。
Crystal
私たちはRubyのこの柔軟性をとても気に入っているので、Crystalでも同じようにしたいと思いました。そこでCrystalでは、デフォルト引数、名前付き引数、可変長引数、型に応じたオーバーロード、メソッドがブロックを受け取るかどうかに基づくオーバーロードをサポートしました。さらに、名前付き引数を必須にしたメソッドを定義して、引数の名前に応じてオーバーロードすることすら可能です。
# `*`の後の引数は名前付きで渡さなければならない
def foo(*, x)
"I got an x: #{x}"
end
def foo(*, y)
"I got a y: #{y}"
end
foo x: 1 # => "I got an x: 1"
foo y: 2 # => "I got a y: 2"
実は、本記事冒頭のRubyコードスニペットは、Crystalでもコンパイルして問題なく動かせますし、振る舞いもRubyとほぼ同じです(chomp
引数はRubyではデフォルトでfalse
ですが、Crystalではデフォルトでtrue
という点は異なりますが)。
このように、APIを楽しく定義できるRubyの柔軟性は、Crystalでもすべて使えるのです。
次回予告
次回はRubyの標準ライブラリの優秀さについてお話ししたいと思います↓。
概要
原著者の許諾を得て翻訳・公開いたします。