Crystal言語作者がRubyを愛する理由(3)ほぼすべてがメソッド呼び出し(翻訳)
Ruby言語公式ページに、Matzによるこんな素敵な言葉が引用されています。
Rubyの外観はシンプルです。けれど、内側はとても複雑なのです。 それはちょうど私たちの身体と同じようなものです。
www.ruby-lang.org/ja/about/より
私が好きなのはRubyのこのシンプルさです。
「Rubyのコードを開くと何が見える?」と聞かれたら、私なら「いろんなメソッド呼び出しが見える」と答えます。
以下のコードを見てみましょう。
require "set"
class MySet
include Enumerable
attr_reader :inner_set
# ...
end
上のコードではこんなことが行われています。
- 外部コードを
require
している class
キーワードでクラスを宣言している- 別のモジュールの機能を
include
している attr_reader
でリードオンリーのプロパティを宣言している
この中でclass
だけが特別です。class
はクラスを定義するための言語の構成体(construct)です。しかしrequire
はRubyで定義されているメソッドへの呼び出しです。同様にinclude
もattr_reader
もメソッド呼び出しです。これらは一見特殊なキーワードのように見えますが、本当は普通のメソッドに過ぎません。
コード内にあるさまざまなものの見た目がほとんど同じで、しかも一貫しているからこそ、Rubyのユーザーが言語キーワードのように見える普通のメソッドを定義することも可能なのです。
実は、attr_reader
を自分で定義することも簡単です。ただattr_reader
は既に存在しているので、代わりにgetter
という名前にしてみましょう。
class Module
def getter(name)
symbol = "@#{name}".to_sym
define_method(name) do
instance_variable_get(symbol)
end
end
end
class Point
getter :x
getter :y
def initialize(x, y)
@x = x
@y = y
end
end
point = Point.new 1, 2
point.x # => 1
point.y # => 2
これでできあがりです!
getter
の使い方はシンプルです。定義はそこまでシンプルではありませんが、それでも定義は可能なのです。
メソッドというよりキーワードのように見えるのは、Rubyではメソッド呼び出しに丸かっこ()
を付ける必要がないからでしょう。仮に丸かっこ()
が必須だとしたらどうなるかを想像してみてください。
require("set")
class MySet
include(Enumerable)
attr_reader(:inner_set)
# ...
end
これではもうキーワードらしくありませんね。私の推測ですが、Rubyで丸かっこ()
を省略可能にしたのはまさにこれが理由だと思います。Rubyは、「キーワードのようなもの」を使うかどうかについては関知しないので、コードのどの場所でも丸かっこ()
を省略できます。
丸かっこ()
の省略はRubyで多用されています。以下はRailsのActive Recordのコードです。
class User < ApplicationRecord
has_many :posts
end
正直、私がRuby on RailsとRubyを学び始めた頃に最初に出会ったコードスニペットもこういうものだったかもしれません。丸かっこ()
の省略機能を使えば、ユーザー向けのミニ言語を作れます。そうした言語はいずれも、メソッド呼び出しを多用する比較的使いやすいものになります。メソッド名や引数の数はさまざまですが、どれも最終的にはメソッド呼び出しなのです。
これをもっと一貫した形でうまく実現した言語がElixirです。Elixirでは以下のようにモジュールを定義します。
defmodule SomeModule do
# ...
end
関数は以下のように定義します。
def method do
end
if
は以下のように書きます。
if something do
end
末尾にいつもdo
があることにご注目下さい。これらはいずれもElixirで定義された関数なのです(厳密にはマクロですが)。たとえば、defmodule
のドキュメントでソースコードを見られます。これはうまくできていますね!
つまり、ElixirもRubyと同様に、ほぼあらゆることを小さな言語(呼び出し)で行うということです。
他の言語でも似たようなことはできるようですが、そのために別の構文を使っていたりします。たとえばRustではマクロ呼び出しの末尾に!
を付けます。
format!("Hello, {}!", "world");
Juliaの場合は@
でマクロを呼び出します。
macro sayhello()
return :( println("Hello, world!") )
end
@sayhello()
D言語ではmixin
キーワードでコンパイル時にコードを生成します。
template GenStruct(string Name, string M1)
{
const char[] GenStruct = "struct " ~ Name ~ "{ int " ~ M1 ~ "; }";
}
mixin(GenStruct!("Foo", "bar"));
ささいな違いに思えるかもしれませんが、ユーザー目線では、「これはマクロだからこう使わないといけない」という具合に、自分が使おうとしているものがマクロなのかそうでないかを意識しなければならないということでもあります。RubyやElixirなら、単なる呼び出しで済みます。
Carole Kingの名曲「You've Got a Friend」に、これにぴったりの歌詞があります。
Winter, spring, summer or fall
All you have to do is call(ただ呼び出せばいいんだよ)
「Rubyのおかげで...友だちができた(you've got a friend)😌」
訳注
Crystal言語では、マクロ呼び出しに別の構文を使うことも検討しましたが、最終的にメソッド呼び出しと同じ構文にしました。結果には大変満足しています。以下のCrystalのコードをご覧ください。
record Point, x : Int32, y : Int32
point = Point.new(1, 2)
record
は、まるで2つのプロパティを持つ型を定義するキーワードのようですが、実際にはマクロです(以下のドキュメントをご覧ください)。
Top Level Namespace - Crystal 1.3.2
比較として、Java 14は「レコード」を言語に追加するために新しいキーワードを導入しました(この名前はどこから来たのでしょう?😮)。
record Rectangle(float length, float width) { }
仮に、このような定型文を減らす方法がJava言語そのものに備わっていれば、キーワードを増やす必要はなかったでしょうし、自動的にAPIドキュメントになるというおまけもついてきたでしょう。
このような構成要素をメソッドやマクロにするもうひとつのメリットは、少なくともRubyやCrystalではユーザーが再定義できることです。たとえばzeitwerkはrequire
を再定義することで動作を改善しています。他の言語でこんなことができるものはあるでしょうか?
次回予告
次回は「完璧なAPIを設計できること」をお題にします↓。
概要
原著者の許諾を得て翻訳・公開いたします。