Edited at
ScalaDay 9

Scala 2.12のゆるふあな話をするよ!(Partial Unification for Type Constructor Inference)

More than 1 year has passed since last update.

遅くなりましたが、この記事はScala Advent Calendar 2016の12/9です。

「ゆるふあに」ということで本当は2.12でのEitherの右傾(right-biased)化の話をしようと思っていたのですが、前日にEitherの変更点については紹介されていたのでもっとゆるふあな2.12の話をします!


の前に1点だけEitherの話を

EitherflatMapが定義されたことによって、以下のコードが通るようになりました。

val ei: Either[Int, String] = for {

a <- Right((42, "xxx")).right
(b, c) = a
d <- Right(0.1).right
} yield s"$c: ${b + d}"

このコード、2.12では.rightが無くても全く同じ結果なので2.11でも通りそうな気がしますが、「EitherにはflatMapなんてないよ!」というコンパイルエラーになります。

なぜかは謎です。ゆるふあです。

EitherにはwithFilterが定義できないので、パターンマッチが使えないという困った感じでした。

それが2.12ではエラーメッセージの通りEitherflatMapが定義されたため、上記コードがコンパイル可能になっています。うれしいですね。


ゆるふあな2.12の新機能(型コンストラクターの推論に対する部分的単一化)について

こちらの機能、まだコンパイルオプションに-Y系である-Ypartial-unificationを設定しないと実行できません。


ゆるふあですね。

build.sbtに記述を追加しましょう。

この機能は高階型と共に使う機能なので、一緒にhigherKindsオプションも入れてしまいましょう。

(本番のコードでは、使用する部分だけscala.language.higherKindsをインポートするほうがお薦めです。)

あと、今回はscalazを使用してサンプルを書いているので、試してみるなら入れておいてください。


build.sbt

scalacOptions ++= Seq(

"-Ypartial-unification",
"-language:higherKinds"
)

libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.2.8"


さて、サンプルなのでsbt consoleを起動して簡単にscalazのFunctor#mapを再定義します。


2.12

scala> import scalaz._, Scalaz._

import scalaz._
import Scalaz._

scala> def map[F[_], A, B](fa: F[A])(f: A => B)(implicit F: Functor[F]) = F.map(fa)(f)
map: [F[_], A, B](fa: F[A])(f: A => B)(implicit F: scalaz.Functor[F])F[B]

scala> map(Option(42))(a => (a + 1).toString)
res1: Option[String] = Some(43)


ここまでは2.11系と同じですね。

これに、Eitherを渡してみましょう。


2.12

scala> map(Option(42).toRight("left"))(a => (a + 1).toString)

res2: scala.util.Either[String,String] = Right(43)

なんと!普通に動くのです!!!

バージョンを2.11に戻して同じコードを貼り付けて見ましょう。

2.11のscalazは2.12のものと比べて非常にダウンロードに時間がかかりますね・・・。

あと、そのままbuild.sbt上でscalaのバージョンだけ下げたら-Ypartial-unificationなんてオプションはないぞ!と起こられました。


2.11

scala> import scalaz._, Scalaz._

import scalaz._
import Scalaz._

scala> def map[F[_], A, B](fa: F[A])(f: A => B)(implicit F: Functor[F]) = F.map(fa)(f)
map: [F[_], A, B](fa: F[A])(f: A => B)(implicit F: scalaz.Functor[F])F[B]

scala> map(Option(42))(a => (a + 1).toString)
res0: Option[String] = Some(43)

scala> map(Option(42).toRight("left"))(a => (a + 1).toString)
<console>:19: error: type mismatch;
found : Product with Serializable with scala.util.Either[String,Int]
required: ?F[?A]
Note that implicit conversions are not applicable because they are ambiguous:
both method ToAssociativeOps in trait ToAssociativeOps of type [F[_, _], A, B](v: F[A,B])(implicit F0: scalaz.Associative[F])scalaz.syntax.AssociativeOps[F,A,B]
and method ToBitraverseOps in trait ToBitraverseOps of type [F[_, _], A, B](v: F[A,B])(implicit F0: scalaz.Bitraverse[F])scalaz.syntax.BitraverseOps[F,A,B]
are possible conversion functions from Product with Serializable with scala.util.Either[String,Int] to ?F[?A]
map(Option(42).toRight("left"))(a => (a + 1).toString)


おわかりでしょうか?


Either[A, B]は型パラメーターが2つあるのでF[_, _]であって、引数のF[_]には合わないということです。


2016/12/14 追記

これをコンパイルが通るようにするには以下のように記述する必要があります。


2.11

scala> type StringEither[A] = Either[String, A]

defined type alias StringEither

scala> val a: StringEither[Int] = Option(42).toRight("left")
a: StringEither[Int] = Right(42)

scala> map(a)(x => (x + 1).toString)
res1: StringEither[String] = Right(43)


このようにStringEither[A]という型を作ってその型で渡すか、以下のようにf[A]という型を一時的に定義して、そのfF[_]に渡すと明示する必要があります。


2.11

scala> map[({ type f[A] = Either[String, A] })#f, Int, String](Option(42).toRight("left"))(a => (a + 1).toString)

res2: scala.util.Either[String,String] = Right(43)


2.12で追加された-Ypartial-unificationは、複数の型パラメーターを持つ型のインスタンスが来た時に、勝手に目的の型にあう形になるまで左側を固定してくれるのです。

ついにという感じですね。

例1:

// 受け取り側の型
F[A]
// 渡す側の型
SomeType[Int, String, Byte]
// => 解釈
F[_] = SomeType[Int, String, _]
A = Byte

例2:
// 受け取り側の型
G[A, B]
// 渡す側の型
SomeType2[Int, Char, List[String], Byte]
// => 解釈
G[_, _] = SomeType2[Int, Char, _, _]
A = List[String]
B = Byte

受け取る側の型に合うまで左から順に埋められていくので、Aの部分に当たるようになるのは常に右側です。

あくまで関数呼び出しの解釈途中でだけで、このまま型パラメーター指定ができるようになったわけではないので注意が必要です。


ゆるふあ機能の使い道

使い道としては、モナドトランスフォーマーとかで型指定をしなくてもうまくいくようになった辺りかなぁと。


2.11

scala> import scalaz._, Scalaz._

import scalaz._
import Scalaz._

scala> val a: Either[Int, Option[String]] = Right(Some("foo"))
a: Either[Int,Option[String]] = Right(Some(foo))

scala> val b = OptionT(a)
<console>:18: error: no type parameters for method apply: (run: F[Option[A]])scalaz.OptionT[F,A] in object OptionT exist so that it can be applied to arguments (Either[Int,Option[String]])
--- because ---
argument expression's type is not compatible with formal parameter type;
found : Either[Int,Option[String]]
required: ?F[Option[?A]]
val b = OptionT(a)
^
<console>:18: error: type mismatch;
found : Either[Int,Option[String]]
required: F[Option[A]]
val b = OptionT(a)
^



2.12

scala> import scalaz._, Scalaz._

import scalaz._
import Scalaz._

scala> val a: Either[Int, Option[String]] = Right(Some("foo"))
a: Either[Int,Option[String]] = Right(Some(foo))

scala> val b = OptionT(a)
b: scalaz.OptionT[[+B]Either[Int,B],String] = OptionT(Right(Some(foo)))


便利ですね!!