昨日は、Jeremy Gibbons さんによる論文を 2本飛ばし読みしておりがみプログラミングと applicative な走査をみた。今日は何かを読む代わりに、Scalaz の使い方に焦点を当ててみる。
Scalaz は implicit を使い倒している。ライブラリを使う側としても、拡張する側としても何がどこから来てるかという一般的な勘を作っていくのは大切だ。Scala の import と implicit を手早く復習しよう!
Scala では import は 2つの目的で使われる: 1. 値や型の名前をスコープに取り込むため。 2. implicit をスコープに取り込むため。
implicit には僕が考えられる限り 4つの使い方がある: 1. 型クラスインスタンスを提供するため。 2. メソッドや演算子を注入するため。(静的モンキーパッチ) 3. 型制約を宣言するため。 4. 型の情報をコンパイラから取得するため。
implicit は以下の優先順位で選択される: 1. プレフィックス無しでアクセスできる暗黙の値や変換子。ローカル宣言、import、外のスコープ、継承、および現在のパッケージオブジェクトから取り込まれる。同名の暗黙の値があった場合は内側のスコープのものが外側のものを shadow する。 2. 暗黙のスコープ。型、その部分、および親型のコンパニオンオブジェクトおよびパッケージオブジェクト内で宣言された暗黙の値や変換子。
まずは import scalaz._
で何が import されるのかみてみよう。
まずは、名前だ。Equal[A]
や Functor[F[_]]
のような型クラスは trait として実装されていて、scalaz
パッケージ内で定義されている。だから、scalaz.Equal[A]
と書くかわりに Equal[A]
と書ける。
次も、名前だけど、これは型エイリアス。scalaz
のパッケージオブジェクトは @@[T, Tag]
や Reader[E, A]
(ReaderT
モナド変換子を特殊化したものという扱い) のような主な型エイリアスを宣言する。これも scalaz.Reader[E, A]
というふうにアクセスすることができる。
最後に、Id[A]
の Traverse[F[_]]
や Monad[F[_]]
その他への型クラスインスタンスとして idInstance
が定義されているけど、気にしなくてもいい。パッケージオブジェクトに入っているというだけで暗黙のスコープに入るので、これは import しても結果は変わらない。確かめてみよう:
scala> scalaz.Monad[scalaz.Id.Id]
res1: scalaz.Monad[scalaz.Id.Id] = scalaz.IdInstances$$anon$1@fc98c94
import は必要なしということで、うまくいった。つまり、import scalaz._
の効果はあくまで便宜のためであって、省略可能だ。
だとすると、import Scalaz._
は一体何をやっているんだろう? 以下が Scalaz
object の定義だ:
package scalaz
object Scalaz
extends StateFunctions // Functions related to the state monad
with syntax.ToTypeClassOps // syntax associated with type classes
with syntax.ToDataOps // syntax associated with Scalaz data structures
with std.AllInstances // Type class instances for the standard library types
with std.AllFunctions // Functions related to standard library types
with syntax.std.ToAllStdOps // syntax associated with standard library types
with IdInstances // Identity type and instances
これは import をまとめるのに便利な方法だ。Scalaz
object そのものは何も定義せずに、trait をミックスインしている。以下にそれぞれの trait を詳しくみていくけど、飲茶スタイルでそれぞれ別々に import することもできる。フルコースに戻ろう。
import は名前と implicit を取り込む。まずは、名前だ。StateFunctions
はいくつかの関数を定義する:
package scalaz
trait StateFunctions {
def constantState[S, A](a: A, s: => S): State[S, A] = ...
def state[S, A](a: A): State[S, A] = ...
def init[S]: State[S, S] = ...
def get[S]: State[S, S] = ...
def gets[S, T](f: S => T): State[S, T] = ...
def put[S](s: S): State[S, Unit] = ...
def modify[S](f: S => S): State[S, Unit] = ...
def delta[A](a: A)(implicit A: Group[A]): State[A, A] = ...
}
これらの関数を取り込むことで、get
や put
がグローバル関数であるかのように扱うことができる。何で? これが 7日目に見た DSL を可能にしている:
for {
xs <- get[List[Int]]
_ <- put(xs.tail)
} yield xs.head
次も名前だ。std.AllFunctions
もそれ自体は trait のミックスインだ:
package scalaz
package std
trait AllFunctions
extends ListFunctions
with OptionFunctions
with StreamFunctions
with math.OrderingFunctions
with StringFunctions
object AllFunctions extends AllFunctions
上のそれぞれの trait はグローバル関数として振る舞う様々な関数をスコープに取り込む。例えば、ListFunctions
はある特定の要素を 1つおきに挟み込む intersperse
関数を定義する:
scala> intersperse(List(1, 2, 3), 7)
res3: List[Int] = List(1, 7, 2, 7, 3)
微妙だ。個人的には注入されたメソッドを使うので、これらの関数は僕は使っていない。
IdInstances
という名前だけど、これは Id[A]
の型エイリアスも以下のように宣言する:
type Id[+X] = X
名前はこれでおしまい。import は implicit も取り込むけど、implicit には 4つの使い方があると言った。特に最初の 2つ、型クラスインスタンスとメソッドや演算子の注入が大切だ。
これまでの所、僕は意図的に型クラスインスタンスという概念とメソッド注入 (別名 enrich my library) という概念をあたかも同じ事のように扱ってきた。だけど、List
が Monad
であることと、Monad
が >>=
演算子を導入することは 2つの異なる事柄だ。
Scalaz 7 の設計方針で最も興味深いことの 1つとしてこれらの概念が徹底して “instance” (インスタンス) と “syntax” (構文) として区別されていることが挙げられる。たとえどれだけ一部のユーザにとって論理的に筋が通ったとしても、ライブラリがシンボルを使った演算子を導入すると議論の火種となる。 sbt、dispatch、specs などのライブラリやツールはそれぞれ独自の DSL を導入し、それらの効用に関して何度も議論が繰り広げられた。事を難しくするのが、複数の DSL を同時に使うと注入されたメソッドが衝突する可能性だ。
std.AllInstances
は標準 (std
) データ構造に対する型クラスのインスタンスのミックスインだ:
package scalaz.std
trait AllInstances
extends AnyValInstances with FunctionInstances with ListInstances with MapInstances
with OptionInstances with SetInstances with StringInstances with StreamInstances with TupleInstances
with EitherInstances with PartialFunctionInstances with TypeConstraintInstances
with scalaz.std.math.BigDecimalInstances with scalaz.std.math.BigInts
with scalaz.std.math.OrderingInstances
with scalaz.std.util.parsing.combinator.Parsers
with scalaz.std.java.util.MapInstances
with scalaz.std.java.math.BigIntegerInstances
with scalaz.std.java.util.concurrent.CallableInstances
with NodeSeqInstances
// Intentionally omitted: IterableInstances
object AllInstances extends AllInstances
次は注入されるメソッドと演算子。これらは全て scalaz.syntax
パッケージ下に入る。syntax.ToTypeClassOps
は型クラスに対して注入されるメソッドを全て導入する:
package scalaz
package syntax
trait ToTypeClassOps
extends ToSemigroupOps with ToMonoidOps with ToGroupOps with ToEqualOps with ToLengthOps with ToShowOps
with ToOrderOps with ToEnumOps with ToMetricSpaceOps with ToPlusEmptyOps with ToEachOps with ToIndexOps
with ToFunctorOps with ToPointedOps with ToContravariantOps with ToCopointedOps with ToApplyOps
with ToApplicativeOps with ToBindOps with ToMonadOps with ToCojoinOps with ToComonadOps
with ToBifoldableOps with ToCozipOps
with ToPlusOps with ToApplicativePlusOps with ToMonadPlusOps with ToTraverseOps with ToBifunctorOps
with ToBitraverseOps with ToArrIdOps with ToComposeOps with ToCategoryOps
with ToArrowOps with ToFoldableOps with ToChoiceOps with ToSplitOps with ToZipOps with ToUnzipOps with ToMonadWriterOps with ToListenableMonadWriterOps
例えば、[syntax.ToBindOps
] は [F: Bind]
である F[A]
を BindOps[F, A]
に暗黙に変換して、それは >>=
演算子を実装する。
syntax.ToDataOps
は Scalz で定義されるデータ構造のために注入される演算子を導入する:
trait ToDataOps extends ToIdOps with ToTreeOps with ToWriterOps with ToValidationOps with ToReducerOps with ToKleisliOps
IdOps
メソッドは全ての型に注入され、便宜のためにある:
package scalaz.syntax
trait IdOps[A] extends Ops[A] {
final def ??(d: => A)(implicit ev: Null <:< A): A = ...
final def |>[B](f: A => B): B = ...
final def squared: (A, A) = ...
def left[B]: (A \/ B) = ...
def right[B]: (B \/ A) = ...
final def wrapNel: NonEmptyList[A] = ...
def matchOrZero[B: Monoid](pf: PartialFunction[A, B]): B = ...
final def doWhile(f: A => A, p: A => Boolean): A = ...
final def whileDo(f: A => A, p: A => Boolean): A = ...
def visit[F[_] : Pointed](p: PartialFunction[A, F[A]]): F[A] = ...
}
trait ToIdOps {
implicit def ToIdOps[A](a: A): IdOps[A] = new IdOps[A] {
def self: A = a
}
}
興味深い事に、ToTreeOps
も全てのデータ型を TreeOps[A]
に変換して 2つのメソッドを注入する:
package scalaz
package syntax
trait TreeOps[A] extends Ops[A] {
def node(subForest: Tree[A]*): Tree[A] = ...
def leaf: Tree[A] = ...
}
trait ToTreeOps {
implicit def ToTreeOps[A](a: A) = new TreeOps[A]{ def self = a }
}
つまり、これらのメソッドは Tree
を作るためにある。
scala> 1.node(2.leaf)
res7: scalaz.Tree[Int] = <tree>
WriterOps[A]
、ValidationOps[A]
、ReducerOps[A]
、 そして KleisliIdOps[A]
も同様だ:
scala> 1.set("log1")
res8: scalaz.Writer[String,Int] = scalaz.WriterTFunctions$$anon$26@2375d245
scala> "log2".tell
res9: scalaz.Writer[String,Unit] = scalaz.WriterTFunctions$$anon$26@699289fb
scala> 1.success[String]
res11: scalaz.Validation[String,Int] = Success(1)
scala> "boom".failureNel[Int]
res12: scalaz.ValidationNEL[String,Int] = Failure(NonEmptyList(boom))
つまり、syntax.ToDataOps
にミックスインされた trait の多くは全ての型にメソッドを導入して Scalaz のデータ構造を作る。
最後に、syntax.std.ToAllStdOps
があって、これは Scala の標準型にメソッドや演算子を導入する。
package scalaz
package syntax
package std
trait ToAllStdOps
extends ToBooleanOps with ToOptionOps with ToOptionIdOps with ToListOps with ToStreamOps
with ToFunction2Ops with ToFunction1Ops with ToStringOps with ToTupleOps with ToMapOps with ToEitherOps
これは色々面白い事をやっている。BooleanOps
には様々な事への略記法が導入されている:
scala> false /\ true
res14: Boolean = false
scala> false \/ true
res15: Boolean = true
scala> true option "foo"
res16: Option[String] = Some(foo)
scala> (1 > 10)? "foo" | "bar"
res17: String = bar
scala> (1 > 10)?? {List("foo")}
res18: List[String] = List()
option
演算子はとても便利だ。3項演算子は if-else よりも短い記法に見える。
OptionOps
も似たようなものを導入する:
scala> 1.some? "foo" | "bar"
res28: String = foo
scala> 1.some | 2
res30: Int = 1
一方 ListOps
はより伝統的な Monad 関連のものが多い:
scala> List(1, 2) filterM {_ => List(true, false)}
res37: List[List[Int]] = List(List(1, 2), List(1), List(2), List())
僕は、飲茶スタイルという名前の方がいいと思うけど、カートで点心が運ばれてきて好きなものを選んで取る飲茶でピンと来なければ、カウンターに座って好きなものを頼む焼き鳥屋だと考えてもいい。
もし何らかの理由で Scalaz._
を全て import したくなければ、好きなものを選ぶことができる。
型クラスはデータ構造ごとに分かれている。以下が Option
のための全ての型クラスインスタンスを導入する方法だ:
// fresh REPL
scala> import scalaz.std.option._
import scalaz.std.option._
scala> scalaz.Monad[Option].point(0)
res0: Option[Int] = Some(0)
これは Option
に関連する「グローバル」ヘルパー関数も取り込む。Scala 標準のデータ構造は scalaz.std
パッケージの下にある。
全てのインスタンスが欲しければ、以下が全て取り込む方法だ:
scala> import scalaz.std.AllInstances._
import scalaz.std.AllInstances._
scala> scalaz.Monoid[Int]
res2: scalaz.Monoid[Int] = scalaz.std.AnyValInstances$$anon$3@784e6f7c
演算子の注入を一切行なっていないので、ヘルパー関数や型クラスインスタンスに定義された関数を使う必要がある (そっちの方が好みという人もいる)。
型クラスの syntax は型クラスごとに分かれている。以下が Monad
のためのメソッドや演算子を注入する方法だ:
scala> import scalaz.syntax.monad._
import scalaz.syntax.monad._
scala> import scalaz.std.option._
import scalaz.std.option._
scala> 0.point[Option]
res0: Option[Int] = Some(0)
見ての通り、Monad
メソッドだけじゃくて、Pointed
のメソッドも取り込まれた。
Tree
などの Scalaz のデータ構造のための syntax も scalaz.syntax
パッケージ以下にある。以下が型クラスと Scalaz データ構造のための全ての syntax を取り込む方法だ:
scala> import scalaz.syntax.all._
import scalaz.syntax.all._
scala> 1.leaf
res0: scalaz.Tree[Int] = <tree>
標準データ構造の syntax はデータ構造ごとに分かれている。以下が Boolean
に注入されるメソッドや演算子を取り込む方法だ:
// fresh REPL
scala> import scalaz.syntax.std.boolean._
import scalaz.syntax.std.boolean._
scala> (1 > 10)? "foo" | "bar"
res0: String = bar
標準データ構造のための全ての syntax を取り込むには:
// fresh REPL
scala> import scalaz.syntax.std.all._
import scalaz.syntax.std.all._
scala> 1.some | 2
res1: Int = 1
手早く書くつもりが、記事をまるごと使うことになった。 続きはまた、後で。