この記事はtraPアドベントカレンダー8日目の記事です。
初めまして、traPの飯島ゆん担当の飯島ゆんです。可愛い女の子なので、現実世界でも可愛い女の子になりたいと思い、カスタムキャストで自分のなりたい女の子を作りました。
今日は僕の性癖を暴露するよ!!!
— 飯島ゆん (@tkdmokomoko) October 7, 2018
性癖が特定されました……助けてください pic.twitter.com/FHYaLoUrAH
女の子の悩み
ところで、女の子なら誰でも抱えている悩みがあると思います。そう、「静的型付けでありながらRubyのように書きやすく、Cのように素早く動作するプログラミング言語はないのか」という悩みですね!!!
かくいう私もその一人で、Rubyの書き味が好きでよく書いているのですが、正直「もう少し早くならないかな〜」と思うことが多々あります。また、Railsなどを書いているとよくRecordが存在せず返ってきたnilに対してmapを呼び出して
と言った感じで実行時にお亡くなりになることがそこそこあるので、そこらへんを実行せずにコンパイラでチェックしてもらえると嬉しいですよね?ただ、それのせいで書き味が犠牲になるのは嫌です。
果たして人類が絶滅するまでの間に、このような女の子の願望を満たす言語は現れるのでしょうか?(いや、現れない)
神への祈り
私はプログラミング言語の神に長い間祈り続けました。その間私は朝昼晩とおやつ以外は何も食べず、飲み物も1日に1,800mlしか飲まず、さらには音ゲーも1日12クレ(600円)しかしないという過酷な環境に身を置き続けました。
(祈りっている時の様子)
金枠暴龍天復帰!!!!!!!!!!!!!!! pic.twitter.com/q5AnMNJTtb
— 飯島ゆん (@tkdmokomoko) October 23, 2018
そのような私を神が憐れんだのか、ついにある言語の存在が啓示されました。そう、その言語こそが……
Crystal lang
というわけで、前置きはここら辺にしてCrystal言語の紹介やを触ってみての感想などを書いていきたいと思います。
What is Crystal lang?
Crystal langとはどのような言語なのでしょうか?本家のドキュメントを見てみると次のように書かれています。
Crystal is a programming language with the following goals:
- Have a syntax similar to Ruby (but compatibility with it is not a goal).
- Be statically type-checked, but without having to specify the type of variables or method arguments.
- Be able to call C code by writing bindings to it in Crystal.
- Have compile-time evaluation and generation of code, to avoid boilerplate code.
- Compile to efficient native code.
私は日本語も英語もプログラミング言語も読めないタイプの女の子なので上の英文を適当に訳すと、
Crystal langは
- Rubyに似た構文で
- 静的型付けだけど変数や引数に型を明示する必要がなく
- Cの遺産をCrystalから使用できて
- コンパイル時にコード生成ができて
- 実行可能で効率的なバイナリにコンパイルされる
ことを目標とした言語である
と言った感じになります。
ここで、私のような女の子の欲しい言語がなんだったかを思い出してみると、
- Rubyのような書き味で静的型付けの言語
- 素早く動作する言語
というものでした。
そう、Crystal langはこれらの希望にマッチしています!それだけでなく、Rubyで慣れ親しんだメタプログラミングほどとは行かないまでも、ある程度柔軟な動作もできるのです。
ここまでの紹介でなんとなくCrystal langの目指す雰囲気はふわっと伝わったかと思いますが、実際はどんな感じなのかをコードを交えて見ていきたいと思います。
Rubyに似た構文
Crystal langとRubyの構文を比較する上で面白そうな例を書いてきました。
以下に示すのはRubyにおけるFizzBuzzの例です。
fizzbuzz.rbdef fizzbuzz
(1..16).each do |i|
if fizz?(i) && buzz?(i)
p "fizzbuzz"
elsif fizz?(i)
p "fizz"
elsif buzz?(i)
p "buzz"
else
p i
end
end
end
def fizz?(n)
(n % 3).zero?
end
def buzz?(n)
(n % 5).zero?
end
fizzbuzz
特に特筆するところもない普通のFizzBuzzですね。
それでは、このFizzBuzzをCrystal langで書き直してみるとどうなるでしょうか?
どれくらい似た結果になるのかを予想してみてください!
fizzbuzz.crdef fizzbuzz
(1..16).each do |i|
if fizz?(i) && buzz?(i)
p "fizzbuzz"
elsif fizz?(i)
p "fizz"
elsif buzz?(i)
p "buzz"
else
p i
end
end
end
def fizz?(n)
(n % 3).zero?
end
def buzz?(n)
(n % 5).zero?
end
fizzbuzz
完全に一致しましたね。「コピペミスじゃないのか」ですって?とんでもない、実際に実行しても2つは完全に同じ動作をします。
ruby grammer.rb
1
2
"fizz"
4
"buzz"
"fizz"
7
8
"fizz"
"buzz"
11
"fizz"
13
14
"fizzbuzz"
16
crystal grammer.cr #ここでは実行可能ファイルを作成せずにそのまま実行しています
1
2
"fizz"
4
"buzz"
"fizz"
7
8
"fizz"
"buzz"
11
"fizz"
13
14
"fizzbuzz"
16
これらの結果を見て、いかにCrystal langの文法がRubyとよく似ているかを感じ取っていただけると思います。
もちろんCrystal langとRubyとでライブラリ名が違うことや、Crystal langが静的型付けであることなどが原因で、全てのRubyのコードがそのままCrystal langで動くわけではありませんが(そもそもRubyとの互換性を保つために文法を似せているわけではないのでそれはそう)、Rubyに慣れている人にとっては違和感なく読み書きのできる言語になっています。
実行速度
女の子はせっかちなので、実行速度は大事な要素です。Rubyに対する不満として最初にあげた通り、私も例に漏れずせっかちな女の子です。果たしてCrystal langは十分に高速なのでしょうか?
今回は実行速度を求めるために45番目のフィボナッチ数を求めるプログラムを書き、その実行時間を計測しました。今回は定義通りの再帰を用いた方法でフィボナッチ数を求めてみました。このアルゴリズムだと計算量はなので、簡単に実行時間を爆発させることができます。
なお、今回は比較対象としてRubyおよびC言語でも計測してみました。実際のコードは以下の通りです。
fib.crdef fib(n)
return n if n < 2
fib(n - 2) + fib(n - 1)
end
p fib(45)
fib.rbdef fib(n)
return n if n < 2
fib(n - 2) + fib(n - 1)
end
p fib(45)
fib.c#include <stdio.h>
int fib(int n) {
if (n < 2) {
return n;
}
return fib(n - 2) + fib(n - 1);
}
int main(int argc, char *argv[]) {
printf("%d\n", fib(45));
return 0;
}
では、実際の実行時間をみてみましょう。今回はtimeコマンドを用いて実行時間を計測してみました。
まずはRubyの場合です。
time ruby fib.rb
1134903170
113.89 real 112.91 user 0.28 sys
大体114秒という結果になりました。もしあなたがビルゲイツだったら、この待ち時間で600万円稼げます。そうでなくても音ゲーで1譜面遊べてしまいます。
次はCの場合をみてみましょう。
time gcc -O2 fib.c
0.06 real 0.03 user 0.02 sys
time ./a.out
1134903170
5.49 real 5.43 user 0.01 sys
今回はなんとなく最適化してみました(最適化前と比べて1秒ほど早くなりました)。コンパイルにかかる時間を合わせても大体5.5秒とRubyと比べるとかなり早くなりました。これぐらいの実行時間ならかなり高速と言えるのではないでしょうか?
最後にCrystal langの場合はどうでしょうか。
time crystal build fib.cr
0.52 real 0.50 user 0.12 sys
time ./fib
1134903170
5.34 real 5.26 user 0.01 sys
コンパイルにかかった時間がCより長いので合計としてはCに軍配が上がりましたが、単純な実行時間だけを比較するとCrystal langの方が若干高速という結果になりました。
今回の結果により、Crystal langは十分に高速であることがわかっていただけたかと思います。
コンパイラによるチェック
ここまでを読んできた方にはわかっていただけると思いますが、Crystal langのプログラムはRubyのそれとかなり似通っています。なんなら今回挙げた例は全て同じコードになっています。これで本当にコンパイラが型に関するエラーをチェックしてくれるのでしょうか?
次に示すのはRailsなどでよくある、条件にあったレコードを持ってきてそれに対して処理を行うコードの簡略化した例です。
recordsという集合の中で条件にマッチしたものを取ってきて、それらに対して最初の文字を大文字にするという同一の処理を行なっています。
def some_condition
rand(2).even?
end
records = "some_record" if some_condition
p records.capitalize
ここで、some_condition == true
の場合は無事にcapitalizeを呼び出してレコードの先頭を大文字にすることができます。
ruby nil.rb
"Some_record"
しかし、some_condition == false
の場合にはどうなるでしょうか?この場合だとrecords == nil
となってしまい、capitalizeを呼び出そうとした段階で次のような感じで終焉を迎えます。
Traceback (most recent call last):
nil.rb:6:in `<main>': undefined method `capitalize' for nil:NilClass (NoMethodError)
今回の例のコードはちょっと簡略化しすぎているのであれですが、Railsなどを使っていると"条件にあう要素がなかった場合はnilを返す"メソッドを使用する機会が多々あります。
それに対して何も考えずに処理を行おうとして実行時に爆発することはできれば避けたいことですが、人類の多くは鶏なので3歩歩くうちにそんなことを忘れてそのまま処理を行います。そして無残な姿を晒すことになります。
そのようなことを避けるためにもテストをしっかり書いていくことは大事ですが、そんなことをしなくても機械的にチェックしてもらえたら嬉しいですよね?
Crystal langの場合はこうなります。
def some_condition
rand(2).even?
end
records = "some_record" if some_condition
p records.capitalize
上記のコードを実行しようとすると、
crystal build nil.cr
Error in nil.cr:6: undefined method 'capitalize' for Nil (compile-time type is (String | Nil))
p records.capitalize
^~~~~~~~~~
こんな感じでコンパイル時に怒られます。
これはどういう風に怒られているかというと、「recordsという(String | Nill)型(String型かNilという意味)の変数のNilにはcapitalizeメソッドはないよ!」って感じです。これを直すまではプログラムの実行ができないので、実行時にどかーんすることが無くなります。いいですね。
なお、上記のプログラムのコンパイルを通したければこんな感じになります。
def some_condition
rand(2).even?
end
records = "some_record" if some_condition
p records.capitalize if records.is_a?(String)
まとめ
ここまでいくつかCrystal langの魅力について話してきました。本当はまだまだ話すことはあると思うのですが、来週の論文輪講の準備がやばいのでこれくらいにしておきます。この記事を読んで面白そうだなと思った方はぜひ見て触れて驚いて見てください!!!