Scalaz の NaturalTransformation

以下のやりとりなどをして、なんとなく書きたくなったので解説してみます。


↑昨日。
↓こっちは別のやりとり


まず問題です、scala.util.Try[A]scala.Either[Throwable, A] に変換するメソッドは、どのように書けるでしょうか?(Aは任意のA型)


たとえば、以下のような"メソッド"として書けますね

import scala.util._

def try2either[A](t: Try[A]): Either[Throwable, A] = t match {
  case Success(s) => Right(s)
  case Failure(e) => Left(e)
}

では、これをvalで束縛するにはどうすればいいでしょう*1

Scalaにおいて、valは多相になれないので、このようなものを作るためだけに、新しいオブジェクトを一つ生成しないといけません。ここで、ScalazのNaturalTransformationの出番です。NaturalTransformationを使うと以下のようになります。

// new NaturalTransformation[Try, ({type l[a]=Either[Throwable, a]})#l] { と等価

val try2either = new (Try ~> ({type l[a]=Either[Throwable, a]})#l){
  def apply[A](t: Try[A]) = t match{
    case Success(s) => Right(s)
    case Failure(e) => Left(e)
  }
}

~>とか、type l[a] のあたりがとても気持ち悪くていいかんじですね。~>という変な矢印は、NaturalTransformationのaliasです。また、型引数を2つとるものは中置記法でかけるので、あんな気持ち悪い書き方ができます。
さて、こんな気持ち悪い冗長なやり方でvalで定義して、果たして使い道があるのでしょうか?

(他にもあるかもしれませんが)自分が思いつく例を2つあげてみましょう。

1つめは、「綺麗に合成ができる」です。


まず、先ほどのtry2eitherの逆関数を、NaturalTransformationを使って定義してみましょう。

val either2try = new (({type l[a]=Either[Throwable, a]})#l ~> Try){
  def apply[A](t: Either[Throwable, A]) = t match{
    case Right(s) => Success(s)
    case Left(e) => Failure(e)
  }
}

すると、ScalazのNaturalTransformationには、最初からcomposeが定義されているので、以下のように簡単に合成できます。

either2try compose try2either


2つめは、「抽象化した、任意のNaturalTransformertion型を受け取るメソッド」というのが書けるようになります。たとえば、ScalazのStreamTには

def trans[N[+_]](t: M ~> N)(implicit M: Monad[M], N: Functor[N]): StreamT[N, A]

というシグネチャのメソッドがあります。
https://github.com/scalaz/scalaz/blob/v7.0.3/core/src/main/scala/scalaz/StreamT.scala#L29
これは、NaturalTransformertionのような抽象化した型を用意しておかないと、書けないメソッドですね。


さて、なぜかNaturalTransformertionを紹介しましたが、実際そんなに多く使いどころがあるわけではないので、普通のScala使いはおろか、Scalazを覚えたい人も、べつにこれは最初に理解する必要はないと思います。



ところで、sbtにも同じく~>という名前で定義されていて、実際にsbt内部で結構使われています
https://github.com/sbt/sbt/blob/v0.13.0/util/collection/src/main/scala/sbt/TypeFunctions.scala#L37

*1:jvmのtype erasureを悪用して、asInstanceOfを使う方法を除く