ペンギン村 Tech Blog

技術をこよなく愛するエンジニア集団が在住するペンギン村から、世界へ役立つ(かもしれない)技術情報を発信する技術系ブログです。某アラレちゃんが済む村とは一切関係ありません。んちゃ!

Crystal 1.0.0(とRuby)で、がんばるぞい bot を復活させた話。

Tl;Dr

それは何?

f:id:yu_dotnet2004:20210415202946p:plain

構成は?

f:id:yu_dotnet2004:20210415203005p:plain

がんばるぞい bot の歴史

(興味がない方は読み飛ばしてください)

ペンギン村 Slack では昔から がんばるぞい bot が稼働していました。

それは Slack bot と呼ぶには単純すぎるもので、「がんばるぞい!」や「がんばったぞい!」などと書き込むと、ランダムでキャラクターがメッセージを返答してくれるという、まぁがんばる人を応援しちゃうぜ的な bot でした。

そんな、がんばるぞい bot ですが実は2度もスクラッチで書き直されています。

初代: JavaScript / AWS Lambda

もっとも初期のサービスは JavaScript / AWS Lambda で稼働させていました。

ちょうど Lambda や AWS Dash Button などが流行っていたので、とりあえずなんか作ってみようという発想で作られたのが始まりだった、と記憶しています。

まぁ本当にお試しで作っただけなのでコード管理すらしておらず、AWS Lambda 上で直接コードを記述したりしていました。

そのため、今ではコードは一切残っていません。

2代目: Kotlin / Digital Ocean

次は Kotlin で1から書き直され、Digital Ocean で借りた Linux VM 上で稼働させました。DB は MongoDB を利用していました。

その時のモチベーションは「Kotlin の DSL 機能を使いこなしてみたい」というもので、無駄に DSL を活用したコードになっていました。

このときに書かれたコードは結構しっかりしたもので、個人的にも満足していたのですが、Linux VM を常に稼働させていたために ¥4,000/mo という高いランニングコストが課題になっていました。

この bot は1日あたり1分程度の計算リソースしか必要としないことを考えると、恐ろしいほどの無駄遣いであったため長きに渡りリプレースを検討していました。

3代目: Go / GAE

3代目は Go で書き直され GAE 上にデプロイされました。ちゃんとした Admin 画面が用意されたのもこの代においてです。

ちょうどペンギン村の合宿があったので、そのときに作り直した感じですね(懐かしい)。 blog.penginmura.tech

GAE を利用したことで、¥4,000/mo だったランニングコストは ¥8/mo まで削減できました。

ここまで作ったのだから、もう今後は作り直すことは無いだろうと思っていたのですが・・・なんと Go の古いバージョンが GAE でサポートされなくなったため、サービスが停止されてしまいました・・・嫌な事件だったね。

長きにわたる沈黙

3代目の問題は、Go の最新バージョンに対応すれば解決するものではありました。

しかし、以外に対応が面倒なのと、私が Go をどうしても好きになれなかったので、結果としてずっと放置されることになりました。

その間、Swift の AWS Lambda Runtime とかが出たりしたので、せっかくだし久しぶりにサーバサイド Swift でもと思ったりもしたのですが、どうしても重い腰が上がりませんでした。

Crystal 1.0.0 リリースのニュース

そんな中、ある技術ニュースが飛び込んできました。 codezine.jp

えぇ、思いましたよ。「また新しい言語ですかぁ・・・」と。

以前から名前や Ruby に似た構文の言語という噂は聞いていたのですが、(ミーハーな私にしては珍しく)あまり興味が出なかったので、ずっと触らずにいました。

しかし、日本人の方が書かれている Introducing Crystal Programming Language を読んでみたところ、その言語のポテンシャルが高いと感じました。

crystal-jp.github.io

えぇ、思いましたよ。「これは・・・触ってみるしか無い。」と。

Crystal ってどんな言語?

静的型付けを持った Ruby で、C言語並の速さ(を目指している)言語、と言えば殆どすべてのことが伝わると思います。

crystal-lang.org

キャッチアップの初期の頃に書いたメモは以下のようになっていました。

  • Ruby に非常に似た構文で、 C言語なみの速さ(を目指している)
  • シングルバイナリにビルドされるので、実行時のランタイムは不要。
  • メモリは GC 管理で、基本的にプログラマが意識する必要なし。
  • 静的型付けで、フロー型の型推論を備えている。
  • struct、enum もサポートしている。(struct はメモリ確保がスタック)
  • ジェネリクスもサポート。
  • ユニオン型を備えており、いわゆる Optional は String | Nil みたいに定義できる(String? も可)
  • 静的メタプログラミングの仕組みとして、AST レベルのマクロを備えている。
  • アノテーションをサポートしており、クラスやメソッドなどにメタ情報を付与できる。
  • 標準のパッケージマネージャ shards を同梱。(Swift Package Manager 同様に中央リポジトリを持たない)
  • 標準のテストフレームワークとして RSpec ライクのものを同梱。
  • 並列処理は Fiber と呼ばれるグリーンスレッドを搭載し、Go言語のようなチャネルによる通信をサポート。
  • FFI として C言語へのバインディングもサポート。
  • コンパイラは既に self-hosting されており、Crystal で書かれている。

公式の冒頭に載っているサンプルコードを見ると、いかに Ruby の文法に似ているかが分かるかと思います。

# A very basic HTTP server
require "http/server"

server = HTTP::Server.new do |context|
  context.response.content_type = "text/plain"
  context.response.print "Hello world, got #{context.request.path}!"
end

puts "Listening on http://127.0.0.1:8080"
server.listen(8080)

まぁ基本的には最近のモダン言語と同じようなパラダイムであり、Rust の所有権モデルのような画期的な何かが発明された言語ではないかと思います。

しかし、Ruby に非常によく似た構文でシンプルかつ簡潔に記述でき、実行速度について殆ど妥協せず、静的に解決されるマクロを備えた言語、というのは微妙になかったような気がします。

他の言語と比較すると

私の所感を表にしてみました。

機能 Crystal Swift Rust Go Ruby
静的型付け
実行速度
学習コスト
型システム
マクロ(メタプログラミング) - -
エコシステム
シングルバイナリ -

これは厳密な比較ではありませんし、そもそも私は比較できるほどに各言語に精通しているわけではありません(それに Crystal は触り始めたばかりです)。

しかし、私の感じたこと何となく伝わるんじゃないかと思います。

実行速度という面から見ると、Swift / Rust / Go など既にたくさんの選択肢があります。

しかし、何十年というプログラミング研究を捨てたと言われる Go は私からするとやや非力に感じます(今度ようやくジェネリクスが導入されるという噂を聞きましたが)。Rust の学習コストは非常に高いと思いますし、Swift も Rust に比べれば簡単かもしれませんが、初期の頃とは比べ物にならないほど複雑で難しい言語になってしまったと感じます。

エコシステムという面から見ると、今は大体の言語が備えている印象はありますが、初期の頃にサポートされていなかったために 3rd party のツールの爪痕が残っていることも多いように感じます。また、もうちょっと標準で手を貸してほしいと思う部分が不足していることもあると感じます。

マクロは強力であるがゆえか、導入されていない(か部分的にしか導入されていない)言語も多いような気がします。実際、マクロは標準のコードより読みづらくなりますが、Rails の記述量の少なさを見るとマクロやメタプログラミングは非常に強力な道具だと思います。

そうした点について Crystal はどうなのかというと、実行速度についてはC言語並かはさておきとして Ruby よりも遥かに高速で、Ruby と殆ど同じ構文なので学習コストもそれほど高くはなく、テストフレームワークやパッケージマネージャ、フォーマッタ、ドキュメンテーションツールなどのエコシステムは完璧に備わっており、静的型付けでありながら強力なメタプログラミングを可能にするマクロを備えています。

加えて、1.0.0 がリリースされたばかりなのにドキュメントが信じられないほど充実している点も非常に評価に値すると思いました。

まぁ詳しく語りだすと、記事の本筋から外れてしまうので、Crystal についての詳しい記事は機会があれば別途書きたいと思います。

re: がんばるぞい bot

そんなこんなで Crystal の強力さに感心した結果、Crystal でがんばるぞい bot を復活させることにしました。

私は AWS より GCP 派なので、Docker コンテナを手軽にデプロイできる Cloud Run を選択することにしました。

システム構成

冒頭の構成図を再掲します。

f:id:yu_dotnet2004:20210415203005p:plain

大雑把に説明すると以下のような感じです。

  • マイクロサービス構成(Cloud Run)
    • slack:Slack イベント処理用
    • web:Admin画面(Basic Auth)
    • backend:GCP 内に閉じたバックエンドサービス(Ruby製
  • データソース
    • Firestore(既存のがんばるぞい bot のデータが Firestore 管理だったので)
    • Cloud Storage(画像管理用)
  • セキュアストレージ
    • KMS(Slack のアクセストークンなどの管理)
  • スケジューラ
    • Cloud Scheduler(HTTPエンドポイントが叩ければ何でもよかった)
  • デプロイ
    • Cloud Build

まぁ構成としては普通かと思います。

Cloud Run のインスタンス起動は高速(数秒程度)なので、Slack bot としても許容できる範囲でした(さすがに即座には反応できませんが)。

外部公開している Slack アプリでも無いのにトークン管理とかは真面目にやってるので KMS を利用しています(その結果、KMS のコストが一番高いというオチも)。

おそらく疑問に思うのは「なぜバックエンドが Ruby 製なのか?」という点だと思います。

鋭い人は気づいたかもしれませんが、Firestore や Cloud Storage などの GCP の各種サービスにアクセスするためのクライアントライブラリについて、当然ではありますが Crystal 版が用意されていなかったためです。

JSON API によるインターフェースを備えている(らしい)とはいえ、認証などの手間を考えると、さすがに1から作る気にはなりませんでした。

そんなわけでバックエンドは Crystal に似た Ruby を採用してみた、という次第です。

加えて言うなら、Crytal では Kemal という Web フレームワークを利用しているのですが、これが Sinatra インスパイヤになっていたのでバックエンドでは Sinatra を採用してみました。Ruby / Crystal でどのあたりが変わってくるか見極めたかった、ということですね。

kemalcr.com

プロジェクト構成

プロジェクト構成についてはあまり語ることは多くないのですが、シングルリポジトリでマルチモジュール構成を採用しています。

f:id:yu_dotnet2004:20210415203422p:plain

Crystal では shards というパッケージマネージャが標準で用意されていたので、コード重複を防ぐ意味でもマルチモジュール構成を採用しました。例えば、バックエンドの API は slackweb の両方から利用されるので、その部分はモジュール化しているという感じですね。

Crystal を使ってみた感想とか

一言でいうなら「開発における生産性が高い」と感じました。(小並感

Ruby 風のシンプルかつ簡潔な表現でコードを記述できるのに、静的型付けによってコンパイル時に多くのミスが発見できるのはとても強力でした。現時点でインクリメンタルビルドに対応していないこともあってコード補完などはサポートされていないものの、それでも素の Ruby より生産的だと感じました(私は Ruby にそこまで精通していないため誤った記述をすることが多いため)。

標準でフォーマッターが提供されているというの開発時のストレスを減らす大きな要因になっていて、インデントは意識せずにコードを切り貼りして最後にフォーマットをかける、というプログラミングスタイルが私の中で定着しました。

型推論まわりがやや不親切かなと感じたものの、全体的にコンパイルエラーメッセージは丁寧で、基本的に読み解けば原因が分かるものばかりでした。小さなプロジェクトではありますが、コンパイル速度などの不満を感じることもありませんでした。

唯一の例外はマクロまわりのエラーで、慣れてないうちはどのコードが原因でエラーに繋がっているのかわからないこともありました。今後も診断メッセージは改善していくと思いますが、初心者が最初に躓く点がここだろうと私は感じました。(--error-trace というコンパイラオプションが非常に役に立ちます)

おわりに

というわけで、半分 Crystal の宣伝みたいなよく分からない記事になってしまいましたが、最後まで読んでくださった方が何か得るものがあったなら幸いです。

Crystal のライブラリも1つ公開してみたので、よろしければご利用ください。

github.com

現時点では Crystal も未熟な部分はあると思いますが、素晴らしい言語だと感じているので、今後も何かしらの形で貢献していけたらと思う今日この頃です。

P.S.

Crystal についての素晴らしい日本語書籍を無料で公開してくださった著者の方々にこの場を借りてお礼を申し上げます。

crystal-jp.github.io