Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

Fast as C, Slick as Ruby

こんにちは! 最近は寒いせいでバイクのセルが調子悪くとても困っている、 普段はRuby/Railsを書いている広告技術部のサンドバーグです!

この記事はGunosy Advent Calendar 2018 - Qiita、23日の記事になります。 昨日の記事はY_sekkyさんDeepなFactorization Machinesの最新動向 (2018) - Gunosyデータ分析ブログでした!

はじめに

「年末年始は新しいことを試してみよう!」という意気込みで今回の記事を書こうと思いました。

何を触ってみればいいか考えていたところ、同じ広告技術部のくらさわさんが書いた elm に関しての記事を読んで、 最近流行りだしそうな言語を触って見ようと思い立ったところです!

tech.gunosy.io

そこで、今回は界隈で話題性のある言語 (elm) が紹介されている記事に一緒に紹介されている、自分が聞いたことなかった言語を ピックアップしようと思いました。 結果として見つけた記事がこの 明るい未来のある最近流行りだしてる5つの言語 でした。

techbeacon.com

その中にいたのが...

Crystal: Fast as C, Slick as Ruby

crystal-lang.org

Crystalは上のキャッチフレーズの通り、SyntaxはRubyをベースに、C並のパフォーマンスを掲げているコンパイル言語です。

Ruby界隈では Ruby 3 による3 times fasterはよく言われていますが、実際のリリースが見えない中 Fast as C という言葉は すごい魅力的ですよね。

そこで今回の記事はざっくりCrystalを触ってみた感想を書こうと思います!

Syntax

まずはいきなりSyntaxの話からですが、 Syntaxはもう「似ている」のではなく、ほぼ同じと言っても過言ではないと思います。 元にRubyのコードをコピペしても、「ほとんど」の場合動きます!

例をだしますと...

class Foobar
  def foo(num)
    num.to_i.times do
      puts "foobar"
    end
  end
end

Foobar.new.foo(ARGV[0])
➜  ruby put-foobar.rb 5
foobar
foobar
foobar
foobar
foobar
➜  crystal put-foobar.rb 5
foobar
foobar
foobar
foobar
foobar

お気づきだとは思いますが、rubyのファイルもそのままCrystalで実行しても動きます。

ただ、CrystalのSyntaxはあくまでHave a syntax similar to Ruby (but compatibility with Ruby is not a goal).で、 すべてのRubyがCrystalで動かせると言うのが目的ではないということは明確です!

実際に先ほどの例でも ARGVの指定をしなくとも Ruby はプログラムを実行できますが、 Crystalで実行した時に argument を指定しないと ARGV[] で落ちてしまいます。

Unhandled exception: Index out of bounds (IndexError)
  from /usr/share/crystal/src/indexable.cr:698:8 in '[]'
  from put-foobar.rb:9:16 in '__crystal_main'
  from /usr/share/crystal/src/crystal/main.cr:97:5 in 'main_user_code'
  from /usr/share/crystal/src/crystal/main.cr:86:7 in 'main'
  from /usr/share/crystal/src/crystal/main.cr:106:3 in 'main'
  from __libc_start_main
  from _start
  from ???

ただ個人的な意見としては「これぐらい」の違いでしかなかったで、 書くのに全く抵抗がなかったです。 抵抗があるとしたら次の...

Type System

Crystal is statically type checked, so any type errors will be caught early by the compiler rather than fail on runtime. Moreover, and to keep the language clean, Crystal has built-in type inference, so most type annotations are unneeded.

The Crystal Programming Language

今更かもしれませんが、RubyとCrystalは根本的にコンパイル言語とスクリプト言語としての違いがあります。 CrystalはRubyと違って実行前にコンパイルしますし、Rubyのようにruntimeエラーではなく、大体のエラーをcompile時に吐きます。

その一つとしてCrystalはタイプチェックをコンパイル時に行っています。 ここで思い出すがのが Golang のようなコード上での type 指定ですが、CrystalはRuby likeを保つ為に言語にtype interfaceが内蔵されており、 typeの指定がほとんど不必要になっています。

実例として以下のようなaddメソッドを容易しました。

def add(a, b)
  a + b
end

puts add(1, 2)

puts add(1, "2")

typeの違う足し算は無理なので、実行された場合はエラーが発生するのはすぐわかります。

➜  crystal type.rb
Error in type.rb:7: instantiating 'add(Int32, String)'

puts add(1, "2")
     ^~~

in type.rb:2: no overload matches 'Int32#+' with type String
Overloads are:
 - Int32#+(other : Int8)
 - Int32#+(other : Int16)
 - Int32#+(other : Int32)
 - Int32#+(other : Int64)
 - Int32#+(other : Int128)
 - Int32#+(other : UInt8)
 - Int32#+(other : UInt16)
 - Int32#+(other : UInt32)
 - Int32#+(other : UInt64)
 - Int32#+(other : UInt128)
 - Int32#+(other : Float32)
 - Int32#+(other : Float64)
 - Number#+()

  a + b
    ^

Crystalでの実行の場合コンパイル時に正しくエラーで落ちますが、

➜  ruby type.rb   
3
type.rb:2:in `+': String can't be coerced into Integer (TypeError)
    from type.rb:2:in `add'
    from type.rb:7:in `<main>'

Rubyでの実行の場合は想定どおり puts add(1, 2) 実行後でないと TypeErrorが発生しません。

NULL Reference Check

Typeチェックに近い話ですが、Crystalのコンパイル時にnullが呼ばれるかのチェックもしてくれます。 以下の作った例では時々 nil + Stringが発生しうるコードです。

def bar
  if rand(2) > 0 
    "bar"
  end
end

puts "foo" + bar

Ruby実行時はTypeチェックの話と同じく、runtime errorが発生しない限りは正常に動作しますが、

➜  crystal ruby nil.rb
foobar
➜  crystal ruby nil.rb
nil.rb:7:in `+': no implicit conversion of nil into String (TypeError)
    from nil.rb:7:in `<main>'

Crystalでの実行時には (String | Nil) という表記で nil が入ってくるのを判定し、 コンパイル時に正しく落ちてくれます!

➜  crystal crystal nil.rb 
Error in nil.rb:7: no overload matches 'String#+' with type (String | Nil)
Overloads are:
 - String#+(other : self)
 - String#+(char : Char)
Couldn't find overloads for these types:
 - String#+(Nil)

puts "foo" + bar
           ^

Rerun with --error-trace to show a complete error trace.

Typeシステムの話も含め、Crystalではtypeを意識しなくてもコンパイルでのエラーチェックで Rubyで時々起こす単純でも重大な間違いを事前に防げるのがとても良いと思いました。

その他もろもろ

  • 標準パッケージが充実! 一般的なアプリを作るのに使う CSV, YAML, JSON, HTTP などの対応の上、WebSocketも含まれているのでチャットサービスも標準パッケージでつくれます!

  • Concurrency! 今回は触る機会がなかったのですが、CrystalはGolangのようにgreen threads (fibers) をつかっています!

  • C-bindings! Concurrencyと同じく、全く使う機会がありませんでしたが、C-bindingsも使えるそうです。

最後に軽くアプリを実装してみた感想

今回は言語の単純な実装だけではなく、アプリの作成までを試してみたかったので、軽くToDoアプリも作ってみました。 実装の詳細はパッケージに依存するところが大きいので説明は省きますが、パッケージの数とその導入方法が簡単だったのが 印象的でした。

Rubyの影響が大きいのかSinatra/Raillsライクなweb frameworkもありましたし、 MySQL/sqlite はともかく、LevelDBなどのkey value store のDB libraryもすぐに見つけて実装できました。

一つ気になるのはドキュメンテーションが少ないのと、パッケージのメンテナンス頻度が比較的少ない ことでした。コミュニティが小さいこともあるとは思いますが、いっきにパッケージが増えて、それ以降 メンテされていないものが結構多い印象でした。

実際に触ってみたコードは以下のレポジトリにまとめましたので、参考にどうぞ (中途半端な実装ではありますが...)! github.com

まとめ

調査不足なところも多く、個人的には実務での使用は程遠いですが、軽く触ってみて興味をそそる内容が多い言語だったと思います。 ToDoリストも実装が途中でしたし、Amberなどのより充実した web framework を使った webアプリを作って見るのも楽しそうに思えます。

後はCrystalが読んだ記事の言うようにemerging language with a bright futureであるかが気になるところです。