DMM.comの、一番深くておもしろいトコロ。

Ruby のように書きやすく C のように速いプログラミング言語「Crystal」

Ruby のように書きやすく C のように速いプログラミング言語「Crystal」

  • このエントリーをはてなブックマークに追加

はじめに

はじめまして、DMM.comラボのy2k2mtと申します。今回は、当チームが開発を行う時にメインの言語として使用しているプログラミング言語「Crystal」 を紹介したいと思います。

f:id:shimamura-taka:20180510114833j:plain

Crystalを選んだ理由

当チームはRubyの経験があるエンジニアが多く、もちろん、作成するアプリケーションには高いパフォーマンスを求められていたため、Crystalは非常にマッチした言語でした。言語のシンタックスはRubyに大きな影響を受け、Rubyの経験があれば違和感なくコードを書いたり読んだりすることができます。また、Cによるバインディングやイベントループベースのノンブロッキング I/O、FiberとChannelsによる並行処理など、言語自体のパフォーマンスの高さもさることながら、その性質を活かすための仕組みも揃っています。さらに、Crystalは静的型付け言語であり、型推論も行うことができます。また、コンパイラによって検証を行うことも可能で、より堅牢性の高いコードを書くことができます。Crystalは ブエノスアイレス(アルゼンチン)在住のAry Borenszweig氏によって開発され、同地のソフトウェア企業Manas Technology Solutionsのフルタイムエンジニアによって主にメンテナンスや拡張が続けられています。

Rubyのように書きやすいCrystal

CrystalはRubyと非常にシンタックスが似ており、Rubyのコードをコピーアンドペーストしてもほとんどのものが動作します。例えば、class def module includeなどのRubyのシンタックスはCrystal でそのまま使えますし、Emurableのメソッドでは、Rubyのinject がCrystal ではreduceに代わるなど同じ挙動で名称が変わるものもありますが、each map flat_map のほか、pputs もそのまま使えます。

例として、RubyとCrystalの両方で最小のWebサーバを記述してみます。 RubyではSinatraを使用した例を示します。

require 'sinatra'

get '/' do
  'Hello world!'
end

CrystalではKemalを使用した例を示します。

require "kemal"

get "/" do
  "Hello World!"
end

Kemal.run

非常に似ている、というよりもほとんど同じコードで同じ動作をすることがお分かりいただけるかと思います。 繰り返しになりますが、CrystalはRubyのシンタックスから多くの影響を受けていて、非常に記述性の高い言語となっています。

Cのように早いCrystal

CrystalはLLVM上で動作するコンパイル言語であり、CやC++,Rustに匹敵する高いパフォーマンスを出すことができます。 いくつかのベンチマークでも、非常に良い結果を出しています。

https://github.com/tbrand/which_is_the_fastest

https://github.com/drujensen/fib

この高いパフォーマンスを活かすための仕組みとして、簡易に記述できる Fiber と呼ばれる並行処理のための機能が備わっています(CrystalにはThreadのような並列処理のための仕組みはありません)。また、並行処理中のデータの受け渡しを可能とする Channels という機能があります。これらの機能を使うことによって、コールバックやプロミスなどの煩雑になりがちな仕組みやAPIを使用することなく、効率的にイベント処理や非同期IO処理などを記述することができます。さらに、Fiberはスレッドの切り替えではないので、コンテキストスイッチのコストもかかりません。 Fiberを使用する並行処理の簡単な例を示します。Fiberは spawn というキーワードを用いて並行処理を行いたい処理部分を囲うことで使用できます。また、 Fiber.yield はスケジューラにFiberを実行することを指示して完了を待ちます。下記の例では、メインのFiberを含めて3つのFiberを起動しています。

channel = Channel(Int32).new

spawn do
  channel.send(1)
end

spawn do
  puts channel.receive
end

puts "Before"
Fiber.yield
puts "After"

この例を実行すると、下記の様な出力が得られます。

Before
1
After

3つのFiberが同時に起動され、1つ目のFiberはメッセージをChannelに送り、2つ目のFiberがChannelからのメッセージを待ち受けます(つまり、1つ目のFiberがメッセージを送信完了するまで待ち続けます)。そして、3つめのメインのFiberが他のFiberの完了を待ちます。 このように、簡単なコードによって手軽に並行処理を記述することができます。

静的型付け言語であるCrystal

初めに書いたとおり、Crystalは静的な型付け言語であり、型推論などの機能も備えています。型の指定を省略することも可能であり、その場合はコードから推定されるすべての型を抽出して列挙します。例えば、以下のようなメソッドの宣言をした場合、戻り値の型は (Int32 | String) として定義されます。

def one(a : String) 
   if a == "one"
       1
   else
       "1"
   end
end

もちろん、特定の型を指定することも可能です。

def one(a : String) : String
...

上記のメソッドの例に戻り値の型を指定しましたが、この場合は type must be String, not (Int32 | String) となりコンパイルエラーとなります。コンパイラで事前にコードを検証できるということは、曖昧なプログラムの多くをコンパイラで実行前に抽出でき、型に関連するバグの発生を未然に防ぎ、大量のテストコードなどで型に関する検証を自らしなくても済むことを意味します。 また、コンパイルを行うことで実行ファイルを効率化し、実行時間やメモリ使用量などを最小化するよう出力を調整することが可能となります。これは、RubyやPHPなどの動的型付け言語では非常に難しいことです。つまり、コンパイラでコードを検証できるということは、生産性やメンテナンス性、さらにパフォーマンスに大きく寄与するのです。

まとめ

個人的には、Crystalは記述性の高さとパフォーマンスのトレードオフを最も良いバランスで取ることができている言語の一つであると考えています。まだまだドキュメントが少なく、一時期に比べ大きく取り上げられることも少なくなったものの、言語自体の魅力は変わりなく、現在も着々と改善が続けられています。 ライブラリ管理ツール shards を中心として、Webフレームワーク、各種ドライバなど様々なライブラリが開発されており、本番向けのアプリケーションを作成する十分な環境も揃っています。快適なシステム開発言語を探している方へ、当記事がCrystalの採用の一助になれば幸いです。

(当記事で使用しているCrystalのロゴはガイドラインに従って使用しています。)

採用情報

現在、DMM.comでは、エンジニアメンバーを募集しております! 興味のある方はぜひ下記募集ページをご確認下さい! dmm-corp.com