Scalaで型クラス?
久しぶりにいくつか記事を書いたけど,
そもそもScalaで型クラスをシミュレートする方法は,Scalaに関する基本的な知識なのだろうか?
(ここで挙げた記事ではまだ型クラスを使っていないが,後々 Bifunctor
とか (mono)Functor
が出てくる(予定))
型クラス
Haskellを触ってきてください.
Scala で型クラスをシミュレートする
さて,Scala で型クラスをシミュレートする方法を見てみる.
ただ,今回は型クラス制約が implicit parameter に対応するとか,そういう話はしないで,もっと基本的な部分.
その話は,次の機会にでも.
Scala で型クラスをシミュレートする方法を見るには,実際に一つ型クラスとそのインスタンスをいくつか作ってみるのが早い.
恐らく Monoid
が一番簡単だと思われるが,今回は Functor
を作ってみる.
Functor
を選ぶ理由としては,後に説明するが Function1
を Functor
のインスタンスにするには,少しばかり工夫が必要で,この記事を書いた趣旨はそこにある.
ネタバレ(?)をしておくと,以降の内容は
に書かれています.
Functor
Functor
については Haskell を(ry
今回は,Functor
が何か?ではなく,Scala で Functor
を実装するとどうなるのかに重点を置きたいので,この辺に関する基本的な知識は常識ということで.
さっそく,Scala で Functor
を書いてみましょう.
trait Functor[F[_]] { def map[A, B](fa: F[A])(f: A => B): F[B] }
こんな感じになります. 自分のように,Haskell を書けないけど読める人のために Haskell の定義を載せておきます.
class Functor f where
fmap :: (a -> b) -> f a -> f b
Scala の場合は,引数の順番が異なっていることに注意して下さい.
次にインスタンスを定義してみましょう.
準備運動として,List
を Functor
のインスタンスとしてみます.
object FunctorInstance { implicit val listFunctor: Functor[List] = new Functor[List] { def map[A, B](fa: List[A])(f: A => B): List[B] = fa.map(f) } }
グローバルに変数を宣言することはできないので,FunctorInstance
オブジェクトの中に書きました*1.
map
の実装は,Scala の List
には元から map
があるためそれを利用することにします.
これは Haskell だと,
instance Functor [] where fmap = map
こうらしいです(初めて知った).
さて,Haskell ならインスタンスを定義するだけで望みのものが手に入りますが,Scala はそうもいきません.
Scala はオブジェクト指向言語なので,我々が欲っするのは Haskell の fmap (*2) [1, 2, 3]
のように,グローバル(?)な関数として map
を呼ぶのではなく,
List(1, 2, 3).map(_ * 2)
のようにメソッドとして呼びたいと思うのが自然でしょう.
そのためには,少しばかり梃入れが必要です.
以下のようなクラスとオブジェクト*2を追加します.
final class FunctorOps[F[_], A](val self: F[A])(implicit val F: Functor[F]) { final def map[B](f: A => B): F[B] = F.map(self)(f) } object ToFunctorOps { implicit def ToFunctorOps[F[_], A](v: F[A])(implicit F0: Functor[F]) = new FunctorOps[F, A](v) }
さて,これで準備が整いました.
scala> import FunctorInstance._ import FunctorInstance._ scala> import ToFunctorOps._ import ToFunctorOps._ scala> List(1, 2, 3).map(_ * 2) res0: List[Int] = List(2, 4, 6)
これだと,今回定義した map
が呼ばれているのか List
の map
が呼ばれているのかわからない.
別のインスタンスを定義してみましょう.
Function1
(一引数関数)も Functor
になると誰かが言っていた気がします.
さっそく定義してみる.
object FunctorInstance { // 省略 implicit def function1Functor[T]: Functor[T => ?] = new Functor[T => ?] { def map[A, B](fa: T => A)(f: A => B): T => B = t => f(fa(t)) } }
こんな感じにすればよいでしょう.
ここでは kind-projector
というコンパイラプラグインを利用しています.
さて,この実装が正しく動くならば ((_: Int) * 2).map((_: Int) + 1) (2)
を実行すると,5
が得られるはず.
scala> ((_: Int) * 2).map((_: Int) + 1) (2) <console>:18: error: value map is not a member of Int => Int ((_: Int) * 2).map((_: Int) + 1) (2)
Oops. Int => Int
に map
なんてメソッドは無いと言われてしまった.
これは良く考えてみると当然で,Functor
のインスタンスを FunctorOps
に変換するための暗黙的型変換を次のように書いていた.
implicit def ToFunctorOps[F[_], A](v: F[A])(implicit F0: Functor[F]) = new FunctorOps[F, A](v)
ToFunctorOps[F[_], A]
を見ると F[_]
というように,F
は型変数を一つだけ受けとるような型となっているが,
Function1
の型シグネチャは Function1[A, B]
というようになっており,F
に Function1
が来ることができない.
これは困った.そこで,この問題を解決するために Unapply
と呼ばれるものを使おうと思う.
trait Unapply[TC[_[_]], MA] { type M[_] type A def TC: TC[M] def apply(ma: MA): M[A] } object Unapply { implicit def unapplyMAB2[TC[_[_]], M0[_, _], A0, B0](implicit TC0: TC[M0[A0, ?]]) = new Unapply[TC, M0[A0, B0]] { type M[X] = M0[A0, X] type A = B0 def TC = TC0 def apply(ma: M0[A0, B0]) = ma } }
さらに ToFunctorOps
に以下のコードを追加する.
object ToFunctorOps { implicit def ToFunctorOpsUnapply[FA](v: FA)(implicit F0: Unapply[Functor, FA]) = new FunctorOps[F0.M,F0.A](F0(v))(F0.TC) }
さて,これにより何が起こるのか?
コンパイラは,((_: Int) * 2).map((_: Int) + 1) (2)
に出会ったとき,どうにかして map
が呼び出せる形に変換できないが探そうとします.
先程までは,それに失敗していたわけですが,Unapply
と ToFunctorOpsUnapply
を追加したことで Unapply[Functor, Int => Int]
が暗黙的に定義されている(暗黙的に変換できる)か探すようになります.
すると,unapplyMAB2
を使って,
implicit def unapplyMAB2[Functor, Function1[_, _], Int, Int](implicit TC0: Functor[Function1[Int, ?]]) = new Unapply[Functor, Function1[Int, B0]] { type M[X] = Function1[Int, X] type A = Int def TC = TC0 def apply(ma: Function1[Int, Int]) = ma }
TC = Functor
M0 = Function1
A0 = Int
B0 = Int
とすれば,全てが上手く行きそうで,implicit TC0: Functor[Function1[Int, ?]]
は Functor
のインスタンスとして定義していたもの(implicit def function1Functor[T]: Functor[T => ?]
)を使えば暗黙的に変換できるため,この制約も問題無さそうだ.
これにより,
implicit def ToFunctorOpsUnapply[FA](v: FA)(implicit F0: Unapply[Functor, FA]) = new FunctorOps[F0.M,F0.A](F0(v))(F0.TC)
が
implicit def ToFunctorOpsUnapply[Function1[Int => Int]](v: Function1[Int => Int])(implicit F0: Unapply[Functor, Function[Int => Int]]) = new FunctorOps[Function1[Int,?],Int](F0(v))(F0.TC)
となり,先程の List
の場合は暗黙的に渡されていた implicit val F: Functor[F]
を 明示的に F0.TC
として渡すことで,Function1[Int, Int]
に対して map
メソッドを呼び出せる形(FunctorOps
)に変形することができる.
さて,本当にできるようになったか試す.
scala> ((_: Int) * 2).map((_: Int) + 1) (2) res0: Int = 5
問題無さそう.
結論
この話題に関する issue か何かがどっかにあった気がするけど,どこにあるのか忘れた.