ISHOCON1をScalaで書いたお気持ち

アジェンダ

ISHOCON1の初期実装にScalaを追加したよ!

github.com

目次

ISHOCON1にScala実装を追加した

goryudyuma.hatenablog.jp

これの第二弾としてScala実装を追加してみました!

やっぱり、新しく学ぶ言語でなにか作ってみるのは本当に勉強になります。しかもできあがるものがもう仕様が決まっているので、ただただ実装すればいいだけというすごく楽な点がやはり魅力的です。

実装はRubyのものを参考に、フロントエンドはGoにあるテンプレートを参考に実装しました。前回のCrystal実装ではRubyのERBのテンプレートと実装はGoを参考にしたので、今回は入れ替えてみた感じですね。

開発環境

今回、このScala実装はWindowsのWSLとVSCodeを使って書いてみました。前回書いた記事は実は伏線でした。

goryudyuma.hatenablog.jp

簡単なアプリを書くには特に問題なく書けることがわかりました!

VSCodeでターミナルがだせるのもすごく良い点です。

ただ、やはり大規模なアプリになってくると、IDEによる支援がほしくなるなーって思いました。使い分けは大事ですね。

技術スタック

今回、Scalatraっていうフレームワークを使いました。ScalaSinatraっぽく書けるやつですね。Ruby実装がSinatraで実装されていたので、同じように書ける物を選びました。ドキュメントが少し分かりづらいと感じた点以外は、概ねすごくいい感じに使えました!ドキュメントが分かりづらいと感じたのは、単に僕のScala力が低いだけな気がしてます。

テンプレートエンジンにはTwirlってのを使いました。普通に使いやすかったです。

他の言語による実装と合わせるため、O/Rマッパーは使わず、PlainSQLで書ける必要があると考えているので、データベース接続にはPlainSQLが書けるSlinkってやつを使いました。

それからこのscalatra.g8を使ってプロジェクトを立ち上げました。最初に必要なものを用意してくれるのですごく楽でした。

github.com

実装について

Rubyの実装を参考に書いたんですけど、実は少し後悔しています。結果としてすごく遅い実装になってしまったのはいくつか理由があるんですけど、そのうちの一つが動的型付け言語と静的型付け言語の違いです。

Rubyって普通の変数の顔をしながら平気でnilが入ってたりします。別にこれは悪い点ではなく、その特徴により非常に書きやすくなっているのですが、全く同じ動きをしたアプリケーションを静的型付き言語で書こうとすると問題が起きます。すべての変数をScalaで言うOption型にする必要が出てきます。

確実にnilが入っていないと確信できる時以外はすべてnilが入っているかもしれないと仮定しながら書くのは本当に辛いので、一部アレンジとして(ロジックは変えずに)改変しましたが、やりすぎて実装が別物になってしまうのも問題だというところが本当に難しいところです。

例を示します。

gist.github.com

Ruby実装のloginのPOSTの処理だけを抽出しました。

  1. まず、authnticate関数に、nilかもしれないparams['email']とnilかもしれないparams['email']を渡す。
  2. authnticate関数のなかでは、nilかもしれないemailを用いて検索をかける。nil.to_sが""となるのでエラーにはならない。
  3. ユーザーを探すが、見つからなかった場合userはnilとなる。
  4. userがnilでなく、パスワードが入力されたものと正しければ次にすすめるが、だめならIshocon::AuthenticationErrorとなる。(仕様でパスワードが平文保存なのはISUCON的には本質ではないので見逃す)
  5. sessionにuserのidを入れる。
  6. current_userを呼び出す。このときのsession[:user_id]は、この関数だけ見ればnilかもしれないが、authenticate関数を通ってきているので確実にuser[:id]が代入されているのでおそらくnilではない。(が、それをこの関数を見ただけでは保証できない点が怖いといえば怖い)
  7. つづいて、update_last_login関数に、おそらくnilではないuserのidを渡す。
  8. そのユーザーのlast_loginを、現在時刻に更新する。
  9. redirectで/に飛ばす

と、こんな感じの処理になっています。うまくやってますが、nilが出てくる可能性が結構高いです。

Scalaでこれを実装する

さて、Scalanilっぽいことをやろうとしたら、Option型を使ってやる必要がありますが、先程述べたようにいちいち全部の変数をOption型に入れては取り出しってやっていくのは非常に辛い。

アルゴリズム的にNoneになるかもしれないuserは仕方ないとしても、私の実装では最初にauthenticate関数に渡すemailとpasswordはOption型でない普通のString型で渡すように変更したりして、うまく解決を図っています。

ここから少しネタバレになりますけど、authenticate関数でemailからuserインスタンスを作ったあと、user[:id]だけをsessionに保存したあと、userインスタンスを一度破棄し、その後でcurrent_userで先程保存したsessionの中のuserのidからまた同じuserインスタンスを作り出してるんですよね。ここ、authenticate関数がuserインスタンスを返せばそれだけでcurrent_user関数はいらなくなって、Scala実装的にもきれいに書けるんです。だけど、ISHOCON的には「ここでSQLを二回呼んでuserインスタンスを二回生成するのは無駄な処理なので、競技時間中に無くしましょう」的な意図も感じるんです。(深読みしすぎですかね?)。なので、ここのロジックを変えるわけにはいかず、Ruby実装に忠実に二回のSQL文とUserインスタンス生成をScala実装でもやっています。

ここが本当に重要なポイントで、今この実装になっているのは、作問者が意図して遅くしようとしてこの実装になっているのか、それとも普通に実装したらこうなってしまっただけでもっと早くしてもいいのか、区別がつかないんですよね。

普通のアプリケーション開発ではこの問題は起こりえないでしょう。というか問題にすらなりません。読みやすくもなり、無駄なインスタンスを生成しなくもなり、さらに高速化にもつながる、やらない理由はない案件ですから。しかし、ISUCONのクローンを用意している状況となると、意図してこの状況にしている可能性があり、その可能性が排除できない以上、無駄な実装をせざるを得ないです。

かといって、いちいち細かいところまで作問者に問い合わせるのも大変ですし、所詮は遊びでやっているので、自分の思ったとおりにやっています。

実装した結果

Scala実装が書けたあと、普通にベンチマークを回すと、ベンチマーカーの並列度が1と2の時は正の点数になりましたが、並列度を3以上にすると負の点数になってしまいました。ベンチマーカーのデフォルトの並列度は3なので、普通にベンチマークを取れば負の点数になっちゃいます。

例に上げたpostのloginの処理以外にも、Scala的でない実装になっているポイントがいくつかあって、それらを改善することで、(ロジックをそんなに大きく変えなくても、)並列度3でもおそらく正の点数が出るようになると思います。もちろんISHOCON的に遅くなっている部分を直せばもっともっと点数がのびるはずです!ぜひ頑張ってください!

終わりに

今回は気分が乗ったので、Scalaで実装してみました!これからも不定期に気分が乗ったときに、他の言語も追加していきたいと思います。

ISHOCON2も結構楽しみにしていて、最初にあったRuby実装に加えて最近Python実装も増えたのですが、まだGo実装がなくて試せてない感じです。やっぱり競技者として出る時は得意な言語で出たいですしね。じゃあお前が実装しろよって話ですけど、競技者として一度やってから書くほうが絶対にいいんですよね。あとから気付く、「ああ、あの時ああすればよかったのか」感が好きなので。なのでGo実装待ってます!(笑)

最後になりましたが、作問者の@showwinさん、楽しい問題をありがとうございました!