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

Crystal言語作者がRubyを愛する理由(3)ほぼすべてがメソッド呼び出し(翻訳)

概要

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

crystal-lang/crystal - GitHub

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で定義されているメソッドへの呼び出しです。同様にincludeattr_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を設計できること」をお題にします↓。

Crystal言語作者がRubyを愛する理由(4)呼び出しが強力(翻訳)

関連記事

Crystal言語作者がRubyを愛する理由(1)「等しさ」の扱い(翻訳)


CONTACT

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