読者です 読者をやめる 読者になる 読者になる

kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

Scalaに関する誤解と事実を語る

TL;DR

  • 世間のScalaに関するイメージは、昔のままであることが多い
  • 昔のままどころか、最初から間違ったイメージを持たれていることも多い
  • 実際には、既に解決されている問題は多々あるし、改善に向かっていることも多い
  • プロジェクト管理の問題を言語に押し付けているケースもある

はじめに

自分が最初にScalaに触れたのが2005年(Scala 1からカウントした場合)、あるいは2007年(Scala 2以降からカウントした場合)と、Scalaとの付き合いも結構長くなってきましたが、その間に

  • Typesafe社(現Lightbend社)の設立
  • 実質標準ビルドツールとしてのsbtの確立
  • ライブラリのバイナリ後方互換性に関するポリシーの策定
  • 公式ScalaイベントScala Daysのはじまり
  • Play 2 Frameworkの登場
  • Scala Center発足
  • その他色々

がありました。この間、Scalaコミュニティ自身も変化してきており、昔のScalaに対する印象が今のScalaに対しても正しいとは言えなくなっています。そこで、Scalaに関する「誤解」(とは言い切れないのですが、現在はあてはまらないもの)と「事実」についていくつか語っておこうと思います。

誤解1:Scalaコンパイルが遅い

Scalaの欠点の筆頭に挙げられるコンパイル速度の問題ですが、誤解ではありません。たとえば、Javaコンパイル速度と比べると遥かに遅いですし、Scalaプログラマはこのことをよくネタにするくらいです。

ただし、たとえば、Kotlinがコンパイル速度がScalaより速いといった風聞は必ずしも事実に基づかないものです(KotlinとScalaコンパイル速度比較についてはこちら)。

また、これは多くの人が犯している過ちだと思うのですが、Scalaの標準ツールであるsbtは立ち上げたまま様々なコマンドを発行して利用することが前提のシェルでもあり、起動したままコンパイルを続けるとコンパイラの速度は倍以上高速になります。このことを知らないまま、毎回sbtを立ち上げなおしていると、せっかくJVMのwarmupによって高速になったコンパイル速度を無駄にしてしまうことにつながります。

なお、Scala 3のコンパイラになることが決まっているDotty

github.com

は現在のscalacに比べて2倍程度高速らしいです(これについては実測していません)

また、scalacのコンパイルを並列化するというアプローチで、Triplequote社がHydraという並列Scalaコンパイラを提供しています。Hydraは現在はOSSでなく、限られた顧客にのみ提供されているようですが、コンパイラをうまく並列化することによって、10コアのサーバマシンにおいて、大規模プロジェクトのビルドがおよそ6.3倍高速したとのことです。これが典型的なデータであれば、今後コア数がさらに増えることが予想されるなかで、ScalaのビルドもPCの多コア化にともなって高速化することが期待できそうです。

もし、HydraがOSS化されることがあれば、Scalaの(コンパイル)遅い、は過去の話になるかもしれませんね。

誤解2:Scalaは(実行速度が)遅い

これは紛れもない誤解、というか、間違い、なので、はっきりとそのようなことはないと言っておきましょう。

scalacのコンパイルしたコードを逆アセンブルしてみた人ならわかると思いますが、scalacの吐くコードというのは極力JVMの命令を直接呼び出すようになっています。たとえば、基本型がオブジェクトであるといったことから、基本型を使うのにもオブジェクト生成が伴うイメージを持っている方もいるようですが、基本型がオブジェクトであるというのは単にそう取り扱って問題ないように言語が設計されているというだけで、基本的にはJVMの基本型演算命令をそのまま使います。

ですから、Javaで速度的に問題ないケースであれば、Scalaを適用して速度的に問題はありません。

誤解3:Scalaでは記号メソッド(あるいは演算子オーバーロード)が濫用されている

これは、昔、Scalaを紹介するのに、foldLeftの別名である/:メソッドが使われたことの影響もあると思います。また、昔のScalaによるHTTPクライアントライブラリdispatch(-classic)において、記号が非常に多用されたことも影響しているでしょう。

現在の、というか、数年前からScalaコミュニティでは、記号メソッドの濫用については抑制的になっており(のはず)、むやみに記号メソッドを使うことには慎重になっています。たとえば 新しい方のdispatchでは記号メソッドは大幅に減っています。

誤解4:implicitは濫用されがちな機能であり、コードの可読性を下げるので良くない

これも昔のScalaコミュニティにおいてそういう傾向があった、という事実がベースにあるのだと思われます。しかし、現在は、既存の型と新しい型の間の変換を行う用途のimplicit conversionは既にアンチパターンとみなされています。たとえば、Scala 2.8から標準ライブラリにはJavaConversionsという、JavaのコレクションとScalaのコレクションを相互に暗黙的に変換するライブラリがありましたが、Scala 2.12ではこれは非推奨になり、その代わりに、asScala()asJava()などとして明示的に相互変換するための`JavaConvertersを使うべきということになっています。

いわゆるimplicitと呼ばれる機能群の中で現在も必要とされているのは、

  • 型クラス(implicit parameter)
  • 拡張メソッド(implicit class)

の主に二つですが、拡張メソッドは最近の言語ではよく見られるものですし、型クラスもその有用性については(Haskellなどの言語において)十分に実証されています。型クラスについても濫用は可能であるという批判はできますが、それをするにはまず型クラスを理解する必要があるでしょう。

誤解5:ScalazやCatsは、圏論とかなんか難しい知識が必要らしくて怖い

おそらく、ScalazやCatsの位置づけについて誤解があると思われるのですが、これらは別に準標準ライブラリという程ではなく、あくまでもサードパーティのライブラリに過ぎません。使いたくなければ使わなければいいだけの話です。既に導入されたプロジェクトについていくためには、ScalazやCatsを勉強する必要があるでしょうが、それは何のライブラリであっても同じことです。

なお、ScalazとかCatzのライブラリ名に圏論の影響があるのは間違いありませんが、実際に使うときにそれらの知識が必要かというと必ずしも必要ないと思います。

誤解6:Scalaは、手続型プログラミングも可能だと宣伝しているが、実質的には関数型プログラミングのスタイルで書かなければいけない!騙された!

これは誤解かどうかちょっと微妙なところです。というのは、実際には、Scalaで手続型プログラミングをするためのライブラリも多くあるのですが、関数型、とりわけ純粋関数型アプローチに基づいたライブラリが目立つという傾向はあり、そのため、このような感想が出てくるのかなと思います。あまり関数型関数型したものを避けたい場合、The Scala Library IndexというScalaライブラリ専用の検索エンジンで探してみると良いでしょう。

ただ、いくら、Scalaで手続型プログラミングが可能であるといっても、map一発で済む処理をいちいち可変コレクションをループで回していると、さすがにつらい、というか、Scalaで書くメリットがあまりないので、最低限、Scalaのコレクションライブラリは使えるべきだと思います(これは、Scalaに限らず、最近の言語はだいたい汎用コレクションに対する高階メソッドが標準であるので…)。

不変コレクションを多用することで、そのメリットを享受しつつも、DBとのやりとりなどの外界とのインタラクションについては無理やり関数型的に扱わずに、素直に手続型で扱うのが良いというのが現在の自分の意見です。

誤解7:Scala後方互換性を重視しない(あるいは無視する)

これに関しては、まず、ソース互換性(.scala後方互換性)とバイナリ互換性(.classの後方互換性)があることを認識する 必要があります。

その上でいうと、Scalaはソース後方互換性をなるべく維持するように進化してきました。Scala 2.10で コンパイルできるコードは(非推奨なライブラリを使っていなければ)、Scala 2.12でもほとんどのケースでコンパイルを通ります。というわけで、ソース互換性をScala開発チーム含むコミュニティはとても重視しています。

バイナリ互換性の問題について論じましょう。その前に、Scalaのバージョンに関して、

  • メジャー・バージョン: Scala 2.X.YのX(だいたい1年以上の周期でアップデート)
  • マイナー・バージョン: Scala 2.X.YのY(短いと1週間くらい、通常は数週間くらいのペースでアップデート)

です。この定義の上で、まず、同じメジャー・バージョンのScalaに対してコンパイルされたライブラリは、マイナー・バージョンが変わってもメジャー・バージョンが同じScala上では動きます。また、同じメジャーバージョンのScalaに対してコンパイルされたライブラリ同士を混ぜることも可能です。

ここで、「じゃあ、Scalaのメジャー・バージョンが上がった場合、そのためにライブラリを書き直さなければ、新しいScalaで使えないのか?」と思った方はいると思います。

その問題は皆無とは言えないのですが、よく使われているScalaライブラリは、sbtのクロスビルドという機能を使って、最低2世代、多いと3世代のメジャーバージョンに対してライブラリをビルドして対応しています。そのため、新しいScalaのメジャーバージョンが出たときの初期の頃こそ、対応ライブラリが出揃わないという問題はありますが(scala-library <- A <- B <- Cみたいな依存関係があると、Cは、AとBが対応してくれないと新しいScalaのメジャーバージョンに対応したものを出せないといったちょっと面倒な話はあります)、1ヶ月くらいすればだいたい落ち着くので、それほど深刻な問題であるとは思わないです。このとき、ライブラリのメンテナの作業としては、sbtの定義ファイル中の

crossScalaVersions

に、新しいメジャーバージョンを加えてライブラリをpublishするだけで良いので、それほど負荷が高い作業でもありません。Scalaの新しいメジャーバージョンが出た際、飽きたメンテナが自分のライブラリを更新しなくなることはよくありますが、多くの人に使われているものであればだれかがメンテを引き取ることもありますし、そうでなければそのライブラリの役割が終わった、ということだろうと思います。

このように、可能な限りソース後方互換性を保つことを前提とした上で、Scalaのメジャー・バージョン毎のバイナリクロスビルドによって、バイナリ後方互換性の問題に関してはかなり軽減することができていると言えます。もちろん、問題は皆無とは言いませんが、このことが致命的になったケースというのも思い浮かばないのが正直なところです。

なお、完全にバイナリ後方互換を保証するという選択肢もあったと思いますが、それではダメなライブラリはいつまでもダメなまま残ってしまいますし、JVMの新しい機能を利用できないというデメリットもあります。たとえば、Scala 2.12では、Java 8のinterfaceのデフォルトメソッドとラムダ式(というかinvokedynamic)を使うことで、クラスファイル数の削減と、ライブラリレベルでのバイナリ後方互換性を保ちやすくすることができるようになりました(Scala本体のバイナリ互換性を捨てることで、ライブラリのバイナリ互換性がかえって保ちやすくなるというのは面白いところです)。

一方、Kotlinでは、完全なバイナリ後方互換性を今のところ約束しているようですが(当面、Java 6で動作するバイナリを吐くことを明言しています)、これでは新しいJVMの機能は利用できないわけで、それでいいのかなあと思うところです。

たとえば、Kotlinでは、最新の1.1でも、Java 6相当のクラスファイルを出力しなければいけないせいで、実装ありのインタフェースが、実装ファイルとインタフェースファイルにコンパイルされます。これは、ライブラリを更新する際に、インタフェースにメソッドを追加するとバイナリ互換性が壊れてしまうことを意味します。バイナリ互換性を重視しているはずのKotlinで書いたはずのライブラリはバイナリ互換性を維持するのがより難しく、バイナリ互換性をKotlinほど重視していないはずのScalaで書いた(2.12以降)コードは、バイナリ互換性を保つのがより容易になるという皮肉な現象が起きるのです。

誤解ではない:Scalaは様々な書き方を許容するので、読みにくいコードが出来上がることがある

「誤解ではない」と書いた通り、そういう側面はあると思います。ただし、実際のプロジェクトではある程度のコーディング規約を定めるべきであることを考えると、コーディング規約を最初に一つ作る手間が削減できるくらいの意味しかないと思います。

また、今時のプロジェクトにおいて、メンバーがコミットしたコードに対してレビューがされるべき(建前じゃなくて普通にやりますよね?)ということを考えると、突然メンバーが変な書き方のコードを持ち込もうとしたら、それはコードレビューの段階でストップをかけるべき話なのではと思います。

これは、一昨年、とある会社がプロダクトコードをScalaからGolangに置き換えたという記事を見たときにも思ったことなのですが、たとえば <|*|>の意味がわからんという話なら、なぜそのコードをコードレビューではじかないのか、と(メソッド名からScalazのコード呼び出しであることは間違いないのですが、そんなコーディングスタイル全体に影響するライブラリを導入するのにちゃんと合意が取れてないというのはその会社の体制がgdgdなのでは、とか)。

おわりに

さて、だらだらと長い記事をお読みくださりありがとうございました。

自分がこれまでScalaについて観察を続けていて思ったことは、Scalaには今も昔も問題がたくさんありますが、問題が広く認識されれば、コミュニティもちゃんと解決に向かう健全さはある、ということです。最近の自分はあまりScalaコミュニティに対してコミットできていない気がしますが、今年はもうちょっとコミットしたいところです。ではでは。