編集
Dotty 0.26から言語仕様が変わってしまっているので、Scala3.0.0-M3向けに書き直しました。
はじめに
自身の予習を残しておくクソエントリーです。
2020年はScalaが大幅リニューアルされて、Scala3がリリースされるかも。ようやくScala2に慣れてきたというのに・・・という事でDotty(Scalaの実験的実装)で予習を行う。
特に型クラス周り(Scala2でいうimplicit
を使うところ)は大幅に変更になっているので、まずはそこを中心に予習。
幾度となくコンパイルエラーを叩き出してようやくたどり着いたコードなので、間違いもあるかもしれません。
なお、仕様は頻繁に変わっているので、必ずしも今のDottyの言語仕様がScala3に導入されるとは限らない。
予習環境
- dotty 0.26.0-RC1
予習の流れ
- ファンクタ、モナド型クラスを作り
-
Maybe
モナドもどきを作り -
Either
モナドもどきを作り - モナドを利用する流れを確認
Eitherは名前の衝突を避けるためBoth
に変更している。
型クラスの定義
Scalaでは型クラスを定義するのにトレイトを用いる。普通の型クラスではなく、型パラメータF[_]
やM[_]
をとる型クラスを定義しています。
ファンクタやモナドの定義方法は、様々あるので、あくまで一例ですし、これが最適かどうかは勉強不足です。
//ファンクタ型クラス
trait Functor[F[_]]{
def map[A,B](m:F[A])(fn:A => B):F[B]
//Functorになる型(Maybe)にmap生やす
extension [A,B](f:F[A]):
def map(fn: A => B)(using functor:Functor[F]):F[B]=
functor.map(f)(fn)
}
//モナド型クラス
trait Monad[M[_]] extends Functor[M]{
def flatMap[A,B](m:M[A])(fn:A => M[B]):M[B]
def pure[A](x:A):M[A]
//mapのデフォルト実装
override def map[A,B](m:M[A])(fn:A => B):M[B] =
flatMap(m)((x:A) => pure(fn(x)))
//Monadになる型に(Maybe)にflatMapを生やす
extension [A,B](m:M[A]):
def flatMap(fn: A => M[B])(using monad:Monad[M]):M[B] =
monad.flatMap(m)(fn)
}
ポイント1(extension)
従来はimplicit class
を使っていたところが、extension
になった。わざわざRichXXX
などと名前を付けなくても、無名でできる。
//scala2
object Implicits{
implicit class RichInt(x:Int){
def times(action: () => Unit):Unit = for (i <- 0 to x) action()
}
}
//scala3(予定)
object Implicits{
extension (x:Int){
def times(action: () => Unit):Unit = for (i <- 0 to x) action()
}
}
ポイント2(using)
従来はimplicit仮引数
を使っていたところを、using
に置き換わった。
//scala2
def show[T](x:T)(implicit s:Show[T]):String = s.show(x)
//scala3(予定)
def show[T](x:T)(using s:Show[T]):String = s.show(x)
Maybeモナドの実装
//Maybe型をenumで定義
enum Maybe[+A]{
case Just(value:A)
case Empty
//Maybeのメソッド例
def getOrElse[B >: A](_else: => B):B = this match {
case Just(v) => v
case Empty => _else
}
}
object Maybe{
def apply[A](value:A):Maybe[A] =
if (value != null) Just(value) else Empty
//Monad型クラスのインスタンス
given Monad[Maybe] {
//Maybeモナドにおけるpure関数の実装
def pure[A](x:A):Maybe[A] = Maybe(x)
//MaybeモナドにおけるflatMap関数の実装
def flatMap[A,B](m:Maybe[A])(fn: A => Maybe[B]):Maybe[B] = {
m match {
case Just(v) => fn(v)
case Empty => Empty
}
}
}
}
ポイント3(enum)
Scala3でenumが追加される予定。普通の列挙型の使い方以外に、代数的データ型というものの定義に利用できるそうです。
//Scala2
abstract sealed class Maybe[+A]
case class Just[+A](value:A) extends Maybe[A]
case class Empty() extends Maybe[Nothing]
//Scala3
enum Maybe[+A] {
case Just(value:A)
case Empty
}
ポイント4(given)
型クラスのインスタンスを作るときのimplicit
がgiven
に変更になった。こちらも無名でいけるし、型パラメータもとれる。
//scala2
object Maybe{
implicit val maybeMonad = new Monad[Maybe] {
def pure[A](x:A):Maybe[A] = ???
def flatMap[A,B](m:Maybe[A])(fn:A => Maybe[B]):Maybe[B] = ???
}
}
//scala3
object Maybe{
//given maybeMonad as Monad[Maybe] でもいけるが、以下の様に無名でもOK
given Monad[Maybe] {
def pure[A](x:A):Maybe[A] = ???
def flatMap[A,B](m:Maybe[A])(fn:A => Maybe[B]):Maybe[B] = ???
}
}
Eitherモナドの実装
// Scala3
//Eitherに近い型パラメータが2つあるケース
enum Both[+L,+R]{
case LeftCase(err:L)
case RightCase(value:R)
}
object Both{
//LeftCaseとRigthCaseを作る関数
def left[L](err:L):Both[L,Nothing] = LeftCase(err)
def right[R](value:R):Both[Nothing,R] = RightCase(value)
//型ラムダ
type BT[L] = [R] =>> Both[L,R]
//型パラメータを持った型クラスインスタンス
given [L] as Monad[BT[L]]{
def pure[R](x:R):BT[L][R] = RightCase(x)
def flatMap[R,A](m:BT[L][R])(fn: R => BT[L][A]):BT[L][A] = {
m match {
case LeftCase(err) => LeftCase(err)
case RightCase(value) => fn(value)
}
}
}
}
ポイント5(型ラムダ)
これが一番理解に苦しんだ。公式の説明短すぎるやろと。
=>>
でラムダ式の様に記述し型を返す関数の様な物を作れる。Monad型クラスの型パラメータの引数の数は1つ(F[_]
)なのに対し、Bothは型パラメータが2つ。なので、Monad[F[_,_]]
としなければならないところを、カリー化することでアリティを合わせる事ができる。(と理解しているが、難しい)
//scala3
type BT[L] = [R] =>> Both[L,R] //よってBT[String][Int]型 == Both[String,Int]型となる
ポイント6(型ラムダを組み合わせた型クラスインスタンス)
もはや、パズルみたいになっているが・・・。given
で作る型クラスインスタンスは型パラメータも受け取れる。
object Both{
type BT[L] = [R] =>> Both[L,R]
given [L] as Monad[BT[L]] {
def pure[R](x:R):BT[L][R] = ???
def flatMap[R,A](m:BT[L][R])(fn: R => BT[L][A]):BT[L][A] = ???
モナドの利用
これで晴れて、モナドもどきを使える。
object Main{
def main(args:Array[String]):Unit = {
val b1:Both[String,Int] = Both.right(10)
val b2:Both[String,Int] = Both.left("Error!")
val b3 = for {
x <- b1
y <- b2
} yield (x + y)
println(b3) // => LeftCase("Error")
コンパイラの気持ちになる
ここからはコンパイラの気持ちになって、コードをトレースしていきます。
あっているかは、かなり自信がないです。
val b1:Both[String,Int] = Both.right(10)
val b2:Both[String,Int] = Both.left("Error!")
-
def right[R](value:R):Both[Nothing,R]
というシグネチャで、value
は10
なのでR
はInt型
やな。 - なので戻り値の型は
Both[Nothing,Int]
やな -
Both[+R,+L]
で共変だから、具体型がL
,R
のサブタイプでも代入可能やん - ほな
Nothing
はString
のサブタイプなので、b1
のBoth[String,Int]
型はBoth[Nothing,Int]
で代入可能ですやん。 - 同様に
b2
のBoth[String,Int]
型はBoth[Nothing,Int]
型で代入可能っと。
val b3 = for {
x <- b1
y <- b2
} yield(x + y)
- おっ、
yield
のあるfor式
やから、flatMap
,map
に変換しよう。
val b3 = b1.flatMap(x => b2.map(y => x + y))
-
b1:Both[String,Int]
にflatMap
なんていうメソッド定義されてないぞ。 - どこかに拡張メソッドがあるかもしれへんから探しに行こう。
given [L] as Monad[BT[L]]
-
Both
コンパニオンオブジェクトに、Monad[BT[L]]
型のインスタンスがあるけど、これはつまるところMonad[Both[String,_]]
型のインスタンスやな。 - おやMonad型インスタンスに拡張メソッド
extension
があるぞ。
trait Monad[M[_]] extends Functor[M]{
//(中略)
extension [A,B](m:M[A]):
def flatMap(fn: A => M[B])(using monad:Monad[M]):M[B] =
monad.flatMap(m)(fn)
- ここでの
M
はBT[String]
-
extension
のm:M[A]
はm:BT[String][A]
。A
がInt
なら、BT[String][Int] = Both[String,Int]
になり探している目的のb1:Both[String,Int]
と型が一致する。 -
flatMap
見つけたで!
def flatMap(fn:Int => BT[String][Int])(using monad:Monad[BT[String]]):BT[String][Int]
- さらに
BT[L][R]
をBoth[L,R]
に置き換えると・・・
def flatMap(fn:Int => Both[String,Int])(using monad:Monad[BT[String]]):Both[String,Int]
- はて、
monad:Monad[BT[String]]
がないけれど、どこにあるんだろう。 -
using
な引数なので、探しに行こう。
object Both{
//(中略)
//型ラムダ
type BT[L] = [R] =>> Both[L,R]
//型パラメータを持った型クラスインスタンス
given [L] as Monad[BT[L]]{
-
Both
コンパニオンオブジェクトにgiven
でMonad[BT[L]]
インスタンスがあるな。これを暗黙的にわたそう。
という流れではなかろうかと・・・。