というわけで?「The Flying Sandwich Parts; TFSP」と、実際のScalazのスタイルを比較、考察する記事を書く流れですね!
以下に書くのは、自分の理想とかではなく、(eed3si9nさんが提唱した)ある意味理想のスタイル(=TFSP)と現実(= Scalaz)を比較した場合にどうなっているか?という話です。
たしかにTFSPとScalazはかなり共通点はあるが、現実的なパフォーマンスなどを考えた場合に、ScalazとTFSPはある程度異なる点があるので、具体的にどのように違うのか?を説明する感じです。
トレイトやクラスの本文内では型注釈 T の省略を禁止する。
Scala標準ライブラリなどでも見受けられるが、多くは型注釈がついているが、一部省略してしまっている場合もあって、統一されていない。ただ、Scalaz7には同じKindの型クラスをまとめて定義する決まりがあり、その場合
https://github.com/scalaz/scalaz/blob/v7.0.0/core/src/main/scala/scalaz/std/List.scala#L14
implicit val listInstance = new Traverse[List] with MonadPlus[List] with Each[List] with Index[List] with Length[List] with Zip[List] with Unzip[List] with IsEmpty[List] with Cobind[List] with Cojoin[List]
「listInstanceの型」を書いておくと、後から違う型クラスを追加した場合に、左の「listInstanceの型」を追加し忘れるというバグが何度かあったので、実際問題Scalazでは、書かないほうがメリットがある場合も少しあるかもしれない
関数内のローカルの値は型推論を使って定義
これはもちろん、特別に必要がある場合を除き、ほぼ型注釈は書かれていない。
var を避ける
TFSP においては、変数を使うことは非推奨とする。
メソッド内のローカル変数としては、(主にwhile文とともに)一部使われている。また、concurrentのFutureやTimerなどでは、ローカル変数ではなく、クラス内のprivate varがある程度使われている(しばしばvolatileを伴って)
null を使うことを禁止する。代わりに Option[A] を使う。
もちろん基本的には使われていない。
が、一部null.asInstanceOf[A]というテクニックや、Actorにおいてメモリリークを避けるためのvarに代入、など使っている場面もある
TFSP においては、常に else 節を書く。
「else節がない」のはつまり、ifの中で副作用を起こすということだが*1、Scalazにおいてもほぼ使われていない。一部例外的に、elseがないifが使われている場合もある*2
https://github.com/scalaz/scalaz/blob/v7.0.0/core/src/main/scala/scalaz/std/Iterable.scala#L22-L24
TFSP においては、常に yield を書く
これもほぼそのとおりだったはず。Scalazで「yeildが使われていないfor使ってる場所」が思いつかないのだが、だれか見つけたら教えてください。
TFSP は例外よりも Either[A, B] その他の失敗をエンコードするデータ型を使うことを推奨
Scalazでは、独自のEitherを定義しており、Scalaz7からは基本的に標準ライブラリのEitherを使用せずにScalazのEitherを使うことになっている*3
パターンマッチを使って case class を分解する
代数的データ型の表現方法については、もっと色々と細かく考えることができる。例えば
- そもそもcatamorphismで代数的データ型を表し、case classを使わない方法*4
- case classは定義するが、private[scalaz]にして、catamorphismのためのfoldメソッドのみをpublicに公開し、(Scalaz内部以外では)パターンマッチをさせない。varianceを使わせないことも考えると、そうしたほうが都合がいい。「操作のみを公開して、データ構造としての内部実装を公開しない」という意味でも、3よりも(定義が面倒になるが)抽象度が高いと思う*5 *6
- case classもpublicにしてしまう
Scalazにおいては、2番目と3番目が混在している。また、たとえcase class自体をpublicにしたとしても、varianceの関係上、case classのapplyとは別に、専用のメソッドを実装するのがほぼ必須である。たとえばEither型のRightやLeftのapplyを直接使ってしまうと、Either型にならず、サブタイピングを使ってしまうことになり、型推論などで不都合が生じる。そのためメソッド名はrightでも、Either[A, B]型を返すメソッドを用意するということ。
https://github.com/scalaz/scalaz/blob/v7.0.0/core/src/main/scala/scalaz/Either.scala#L387
この問題については、もっと書きたいことがあるが、脱線しすぎるのでこれくらいにしておく。
case class 内にメソッドを定義することを禁止
これは、型クラスとして表せない(表すメリットがそれほどない)メソッドもそれなりあったり、パフォーマンス的なことも考えると、それほど厳密に守られていない。
ただ、
「親のsealed traitにメソッドをすべて定義し、それを継承したcase classにはメソッドはつけない」
というパターンはかなり多い。
クラスよりも trait を推奨する。外部ライブラリへの橋渡し以外の目的では素のクラスは必要無い
*7その通りで、classは必要なくtraitで済む、のでScalazでもほとんどtraitである。が、一部(おそらく、それほど深い理由はなく)classが使われている場合もある
タプルを渡すことが好ましい場合を除いてカリー化された関数をデフォルトのスタイル
これも(主に、パフォーマンス上の理由により)それほど実践されてない。だた、ある程度使われていたり、両方用意してる場合はある。
(_: Int) + 1 のようなプレースホルダー構文を使った無名関数を禁止する
これは、全く気にされてなく、プレースホルダー構文が使える場合は普通に使っている
def よりも関数を使う
ほとんど実践されてない。
オーバーロードの禁止
一時それなりに使われていたが、tonymorrisさんが結構反対して、わざとオーバーロードをしないように変更した箇所がいくつかあり、現在はあまり使われていない。
「なぜオーバーロードを使うべきではないのか?」
を説明したいが、自分も理解が曖昧なので、今はやめておく
型クラスを使ったアドホック多相を推奨
これは、そもそもScalazがまさにそのためのライブラリなので、言うまでもなく型クラスだらけである。
context-bound な型パラメータを受け取る
型クラスつかうなら、もちろん自然と使うことになる。
ところで、context-bound のスタイルで書くか、明示的にimplicit parameterを書くか2通りあるが、あまり統一されてない。
モジュール間の依存性
ここ想定してる"モジュール"とScalazのそれが同じものをさすのかわからないが、Scalazにおいて、型クラスのインスタンスを定義して参照するための機構は、かなり工夫されたある意味複雑な*8ルールが大量にあるのだが、それをここで書くのは長くなりすぎるので略
変位指定を避ける
つい最近、このpull requestがmergeされ、変位指定を基本的に使わないことになった。
Smash variance annotations
この件に関するメーリングリストでの議論
おそらく、version 7.1にとりこまれるはずである。これについても、もっとちゃんと調べて色々書きたいが(ry
だいたい元エントリにしたがって、Scalazのスタイルを説明してみたけれど、まだまだScalaにおいてのスタイルで考慮するべきことはいっぱいあったような気がするが・・・
*1:elseがなくても、AnyまたはAnyValが返るが、それを使う場合はほぼ皆無だろう
*2:varと同様、これも、明らかにそのほうがパフォーマンス的にいい場合など
*3:mailing listもしくはissueで、少しそういう議論があった
*4:定義は綺麗になるが、流石にパフォーマンス的にまずいので、現状ほぼ実用されていない
*5:ただし、case classをmatch式で分解するより、foldを使わせることにより毎回関数オブジェクトが発生して、パフォーマンス的には劣る
*6:例 https://github.com/scalaz/scalaz/blob/v7.0.0/xml/src/main/scala/scalaz/xml/CDataKind.scala
*7:ものすごく細かいパフォーマンス(invoke virtualとかそういうの) http://stackoverflow.com/a/973531/605582 を考慮しなければ
*8:だがしかし、それらにちゃんとした理由がある