11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Scala3の予習(型クラス)

Last updated at Posted at 2020-09-04

編集

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)

型クラスのインスタンスを作るときのimplicitgivenに変更になった。こちらも無名でいけるし、型パラメータもとれる。


//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]というシグネチャで、value10なのでRInt型やな。
  • なので戻り値の型はBoth[Nothing,Int]やな
  • Both[+R,+L]で共変だから、具体型がL,Rのサブタイプでも代入可能やん
  • ほなNothingStringのサブタイプなので、b1Both[String,Int]型はBoth[Nothing,Int]で代入可能ですやん。
  • 同様にb2Both[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)
  • ここでのMBT[String]
  • extensionm:M[A]m:BT[String][A]AIntなら、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コンパニオンオブジェクトにgivenMonad[BT[L]]インスタンスがあるな。これを暗黙的にわたそう。

という流れではなかろうかと・・・。

11
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?