これは Scalaz Advent Calendar 2012 の17日目です。
追記:タイトルとりりろじさんのtweetの引用のあたりを突っ込まれましたが、今から変えるのもあれなので、そのままにしておきます。*1 こっちも読みましょう。
@j5ik2o ScalazにもMonadPlusあって URL MonadPlusをfor式で使って、Haskellのguard関数と同じようなことができるようにするため、filterという名前で定義されてたりして面白いですよ
2012-12-16 20:21:55 via web to @j5ik2o
うたた寝していてtweet返信できなかったのと、この Syntaxという仕組みはかなり重要なんですがわかりづらいところなので、blog書いて説明することにしました。gakuzzzzさんが4日目で Scalaz の命名規則 というのを書いて全体的に説明してくれていますが、その Syntax の部分だけをさらに詳細に説明します。
Syntax というものの一番の目的は「できだけ型推論が効くようにして、Scalazを使う側のユーザーが型を書かなくてもいいように」というのを追い求めた結果です。*2 *3
まず Haskell の話からしましょう。Haskell においては、
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 べつの言い方をすると
へえ scalaz7 には XXXSyntax とかあるのか。(制約付きの)多相的な値をメソッドから返せない(ことはないが自然に書けない)からってことですね。前から import で型省略させろとずっと主張してたんですが、ようやくかあ。 URL
2012-12-04 23:07:48 via web
ここでやっと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を使った同じような仕組みがありました。具体的にいうと、Identity、MA、MABなどです。「なぜ、MAやMABをやめて、少し違う仕組みにしたのか」を話しだすと長くなるし、(Scalaz6を使ったことがなく)これから新たにScalaz7を使うユーザーにとっては、それほど重要じゃない実装詳細なので省きます( そのうち機会があれば説明したい )
あと、syntaxの原理というか全体的な概要の説明だけじゃなく、本当はもっと細かくそれぞれの型クラスのコードを見ていったり、Scalaz7における
- 型クラスの定義
- 型クラスのインスタンス定義
- その型クラスのsyntaxの定義
- その型クラスに対するテスト
という順番で話したかったのですが、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の内部実装全体の話とか、その他まだ話していない細かい工夫は色々とあります