はじめに
ちょっと前に、Scala3の予習として、Dottyで型クラスを使ったモナド実装の記事を書いたが、また言語仕様が変わり、以前のものが動作しなくなったので、改めて以下に記載する。
なおかつ、(私はあまり好みではないが)インデント構文で書いてみる。
内容は以前の記事の方が説明的で、今回はソース中心・・・。
環境
% scalac -version
Scala compiler version 3.0.0-M3 -- Copyright 2002-2020, LAMP/EPFL
ファンクタとモナドの型クラスの定義
Monad.scala
package monad
trait Functor[F[_]]:
extension[A,B](m:F[A])
def map(fn:A => B):F[B]
trait Monad[F[_]] extends Functor[F]:
def pure[A](x:A):F[A]
extension[A,B](m:F[A])
def flatMap(fn:A => F[B]):F[B]
//mapのデフォルト実装
override def map(fn:A => B):F[B] = flatMap(x => pure(fn(x)))
Scala2と比較して
-
implicit class
はextension
になった。
また前(Dotty v0.26)と違い、
-
extension
の中の拡張メソッドで型パラメータが使えなくなっていた。 - 拡張メソッドと同名のメソッドが定義できなくなった。
- そのため、拡張メソッドのみ定義。
Maybeモナドの実装
Maybe.scala
package monad
enum Maybe[+T]:
case Empty
case Just(value:T)
def isEmpty:Boolean = this match
case Empty => true
case Just(_) => false
def getOrElse[A >: T](_else: => A):A =
this match
case Empty => _else
case Just(value) => value
object Maybe:
def apply[A](value: =>A):Maybe[A] =
if (value != null)
Just(value)
else
Empty
given Monad[Maybe] with
override def pure[A](value:A):Maybe[A] = Maybe(value)
extension[A,B](m:Maybe[A])
override def flatMap(fn:A => Maybe[B]):Maybe[B] =
m match
case Empty => Empty
case Just(value) => fn(value)
Scala2と比較して・・・
- 代数型定義に
abstract sealed class
ではなくenum
を利用できる -
implicit def
やimplicit object
,implicit val
の代わりにgiven
で型クラスのインスタンスを定義する。 - その際、無名にできる。
また前(Dotty v0.26)と違って
-
given [名前] as [型クラス]
ではなくgiven [名前]:[型クラス] with
になった(てかこのwith
なんやねん、分かりにくいわ!いらんやろ!)
Eitherモナドもどきの実装
Either2.scala
package monad
//代数的データ型をenumで定義
enum Either2[+L,+R]:
case LeftCase(error:L)
case RightCase(value:R)
object Either2:
def left[L](error:L):Either2[L,Nothing] = LeftCase(error)
def right[R](value:R):Either2[Nothing,R] = RightCase(value)
//Monad[F[_]]の型パラメータ数と合うようにするため型ラムダを定義
type ET2[L] = [R] =>> Either2[L,R]
//Either2のMonadインスタンス
given [L] : Monad[ET2[L]] with
override def pure[R](value:R):ET2[L][R] = right(value)
extension[R,B](m:Monad[ET2[L][R])
override def flatMap(fn:R => ET2[L][B]):ET2[L][B] =
m match
case LeftCase(error) => LeftCase(error)
case RightCase(value) => fn(value)
Scala2と違い
- Type Lambda(型ラムダ)を定義できるようになった
使ってみる
Either2オブジェクトに、map
,flatMap
が拡張メソッドとして存在するのでfor式
で使えるようになる。
Main.scala
import monad._
object Main:
def main(args:Array[String]):Unit =
val e1:Either2[String,Int] = Either2.left("error!")
val e2:Either2[String,Int] = Either2.right(10)
//yieldのあるfor式はmap,flatMapに変換される
val e3 = for
x <- e1
y <- e2
yield
x + y
println(e3) // => LeftCase("error!")
感想
-
given
の後につけるwith
は正直気持ち悪い。なぜ、with
なんだ。 - とはいえ、Scala2の
implicit
は初心者殺しだったので、マシにはなった - 正直いうとインデント構文は以下の理由により、あまり好みではない。
- それでなくても大きくないScalaコミュニティを分断しかねない
- ブレース構文と混ざると読みにくい
- 2種類あると初学者が混乱する
- (今のところ)LinterやFormatterが対応していない