Scalaz の Syntax という仕組みの解説

これは Scalaz Advent Calendar 2012 の17日目です。

追記:タイトルとりりろじさんのtweetの引用のあたりを突っ込まれましたが、今から変えるのもあれなので、そのままにしておきます。*1 こっちも読みましょう。


と、j5ik2oさんが Haskell のことについてつぶやいていたのに、「それScalazにも(ry」というウザイ絡み方をしたら、ちょっとだけ興味を持ってくれたらしく、以下のようなtweetをしていました。



うたた寝していてtweet返信できなかったのと、この Syntaxという仕組みはかなり重要なんですがわかりづらいところなので、blog書いて説明することにしました。gakuzzzzさんが4日目で Scalaz の命名規則 というのを書いて全体的に説明してくれていますが、その Syntax の部分だけをさらに詳細に説明します。


Syntax というものの一番の目的は「できだけ型推論が効くようにして、Scalazを使う側のユーザーが型を書かなくてもいいように」というのを追い求めた結果です。*2 *3



まず Haskell の話からしましょう。Haskell においては、

Functorという型クラスの定義

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Maybe(ScalaでのOptionにあたるもの)のFunctorのインスタンスの定義

instance  Functor Maybe  where
    fmap _ Nothing       = Nothing
    fmap f (Just a)      = Just (f a)

使い方

> fmap (\x -> x + 2) (Just 1)
Just 3

となっています。fmapは


「"a -> b" という関数と、 f a という値を受けとって f bを返す関数」


です。上記のMaybeでの適用例では


"\x -> x + 2"というラムダと、"Just 1" というMaybeの値を渡すと、"Just 3"が返ってきています。


これを Scala というか Scalaz でそのままやろうとすると、Scala型推論が弱いために面倒なことになります。
Haskell において、fmapは「Functorという型クラスのメソッド」でした。Scalazでも概念的にはそうですが、Scala自体はオブジェクト指向でもあるので、Scalaにおいては

  • トップレベルの関数は定義できない
  • すべて、なんらかのオブジェクトに属しているメソッド

です。なので、必ず

オブジェクト.メソッド(引数)

という形になるはずです。*4

つまり、Haskellにおいての

fmap 引数1 引数2

というのを同じようにやろうとすると

なにかオブジェクト.fmap(引数1, 引数2)

になるはずですが、この「fmapが定義されているオブジェクト」はどうすればいいでしょうか?この「オブジェクト」を持ってくるのに以下のように型を指定しないといけなくなります*5 *6 *7

scala> Functor[Option].map(Option(1))(x => x + 2)
res0: Option[Int] = Some(3)

型推論が弱いというか、Haskellとの言語機能の仕組みの違いにより、上記の
Functorのコンパニオンオブジェクトのapplyの型引数
を省略して推論させることができません。*8 べつの言い方をすると

という感じでしょうか。*9

ここでやっとj5ik2oさんが言ってた"MyOptionFunctor.map(MySome(1))(_ + 2)"というコードの話につながるわけです。


コンパニオンオブジェクトのapplyに型を指定して、型クラスのオブジェクトを取得する *10


もしくは


直接、型クラスのインスタンスのオブジェクトの名前を指定


して、そのメソッドを呼ばないといけなくなります。( j5ik2oさんがtweetしていたのは、明示的に名前を指定して呼ぶ方法 )
でもわざわざそんなことしていたら、型を書く場所が大量にでてくるし、抽象度が半減して、Scalazを使う利点があまりなくなってしまいます。*11


そこででてくるのがSyntaxです。たとえば、fpairというメソッドで説明すると*12

scala> Functor[Option].fpair(Option(2))
res0: Option[(Int, Int)] = Some((1,1))

が、Syntaxのおかげで以下のように書けます

scala> Option(2).fpair
res0: Option[(Int, Int)] = Some((1,1))

Option(2).fpairの呼び出しが実際どうなっているかというと以下のようになってます

scalaz.Scalaz.ToFunctorOps[Option, Int](scala.Option.apply[Int](1))(scalaz.Scalaz.optionInstance).fpair

つまり、Haskellにおいて

型クラスの関数 引数1 引数2 ...

という呼び出しが、ScalazのSyntaxでは

引数にあたるオブジェクトのどれか1つ . 型クラスの関数名 ( 引数1 , 引数2 ... )

と、順番が変わります。わかってしまえば、根本的な仕組みはimplicit conversionを使っているだけなので大したことないです*13


syntax自体の説明は以上で、ここから下はちょっとだけ話逸れて余談です。




Scalaz6においても、原理的にはimplicit conversionを使った同じような仕組みがありました。具体的にいうと、IdentityMAMABなどです。「なぜ、MAやMABをやめて、少し違う仕組みにしたのか」を話しだすと長くなるし、(Scalaz6を使ったことがなく)これから新たにScalaz7を使うユーザーにとっては、それほど重要じゃない実装詳細なので省きます( そのうち機会があれば説明したい )




あと、syntaxの原理というか全体的な概要の説明だけじゃなく、本当はもっと細かくそれぞれの型クラスのコードを見ていったり、Scalaz7における

  1. 型クラスの定義
  2. 型クラスのインスタンス定義
  3. その型クラスのsyntaxの定義
  4. その型クラスに対するテスト

という順番で話したかったのですが、Haskellと比較しながら概要だけ説明したらすでにかなり長くなったので、それはまた書く機会があれば別に書きます。



最後に、今回書いたsyntaxの話のほとんど(と、今回話してないその他色々)は以下の公式のdocument
https://github.com/scalaz/scalaz/blob/v7.0.0-M6/README.md
https://github.com/scalaz/scalaz/blob/v7.0.0-M6/doc/DeveloperGuide.md
に書いてあるので、それも読みましょう。

*1: 言い訳 https://twitter.com/xuwei_k/status/280447734778060803

*2: 使う側のユーザーが楽をできるかわりに、実装側では、かなり地味な単純作業が多くなります。それを軽減するためにコード生成が行われています https://github.com/scalaz/scalaz/blob/v7.0.0-M6/project/GenTypeClass.scala

*3: 公式のdocumentのこのあたり https://github.com/scalaz/scalaz/blob/v7.0.0-M6/README.md#syntax に "which can be easier to read, and, given that type inference in Scala flows from left-to-right, can require fewer type annotations." と書いてありますね

*4: importで制御したりpackage objectに定義すれば、オブジェクトの部分は省略できるけど、あくまで見た目上省略できるだけで、常に "なんらかのオブジェクトのメソッド" を呼び出しているはずです

*5: Functor[Option] は implicitly[Functor[Option] ] もしくは scalaz.std.option.optionInstance と同じ

*6: 現状のScalaz7 (7.0.0-M6)では、実際のOptionのFunctorのインスタンスの定義場所はここ https://github.com/scalaz/scalaz/blob/scalaz-seven/core/src/main/scala/scalaz/std/Option.scala#L11-L53 で、"直接Functorのインスタンスだけが定義されているのではなく" ややこしいことになっているが、それを説明すると長くなって話が逸れるので今回は説明しない

*7: Haskell と Scalaz の Functor の fmap では、引数の順番が異なっていることに注意

*8:この部分に関して、"推論できません"の一言では正確じゃないというか明らかに言葉足りてないけど、うまい言葉でてこない

*9: ちなみに、昨日の記事 http://d.hatena.ne.jp/xuwei/20121216/1355656159 でも Apply[Gen ].apply5 と書いていたりしました

*10: Scalaz7においては、それぞれの型クラスのコンパニオンオブジェクトに、必ずそのような型クラスのオブジェクトを取得するための、「implicit引数のみをとるメソッド」がapplyという名前で実装されている

*11: もっと細かい話をすると、いくら命名の規則が決まっていたとしても"名前を直接指定"の方法ではだめで、「型だけを指定して、型クラスのインスタンスを取得」できないと問題がある。"名前を直接指定" の方法は、実際使われることはない

*12: mapという名前のメソッドではScala標準でOption自身に存在していて説明できないので

*13: が、Syntaxに関連したScalazの内部実装全体の話とか、その他まだ話していない細かい工夫は色々とあります