by shigemk2

当面は技術的なことしか書かない

関数型Scala(型クラス編) まとめ #関数型Scala

connpass.com

型クラスの話

型クラス入門

http://halcat0x15a.github.io/slide/functional_scala/#/

抽象化の手法のひとつ

Semigroupを例にScalaでの型クラスをみていく

型クラスはtraitで宣言する

trait Semigroup[A] {
  def append(x: A, y:A): A
}

結合則を満たしている

append(x, append(x,y)) == append(append(x,y),z)
  • 型クラスのインスタンスはimplicitをつけて宣言する
  • 型クラスのインスタンスは型を明示すべきである

Semigroupを使ったメソッドを定義 doubleメソッド 型クラスはカリー化されたimplicit パラメータにとります。

Scalazだとこんな感じの命名規則

implicitパラメータは型パラメータと同名の変数を付けることがある

implicitがスコープ内にあればdoubleメソッドは次のように呼び出すことができる 故に、IntかStringかどうかはあまり選ばない

assert(double(2) == 4)
assert(double("hoge") == "hogehoge")

implicitパラメータを明示しない記法も存在する

def quadruple[A: Semigroup](a: A) = double(double(a))

これはContext Bounds(暗黙的に引数がつくので、引数を省略できる)

二項演算は演算子として定義

implicit class SemigroupOps[A](x: A)(implicit A: Semigroup[A]) {
  def <>(y: A): A = A.append(x, y)
}

assert(1 <> 2 <> 3 == 6)
assert("foo" <> "bar" <> "baz" == "foobarbaz")

いろいろな方法からimplicitパラメータを検索できる

Monoidを例にimplicitの検索についてみてみる

モノイド - Wikipedia

trait Monoid[A] extends Semigroup[A] {
  def zero: A
}

メソッド定義(畳み込みでappendするメソッド アスタリスクで可変長引数を表現)

def append[A](xs: A*)(implicit A: Monoid[A]): A = xs.foldLeft(A.zero)(A.append)

ListをMonoidで抽象化してみる

implictはdefで宣言することで型パラメータをもつことが可能である

object Semigroup {

  implicit def listMonoid[A]: Monoid[List[A]] =
    new Monoid[List[A]] {
      def zero: List[A] = Nil
      def append(x: List[A], y: List[A]): List[A] = x ::: y
    }

}

こんな感じで使えます

assert(double(List(0, 1, 2)) == List(0, 1, 2, 0, 1, 2))

assert(append(List(0), List(1), List(2)) == List(0, 1, 2))
  • SemigroupとMonoidのどちらに対してもインスタンスを提供できる
  • 型クラス→スーパークラスのコンパニオンオブジェクト
  • MonoidはSemigroupに依存している 依存関係がよくわからない
  • 型クラスのコンパニオンオブジェクトとスーパークラスのコンパニオンオブジェクトだと検索順序があるのでは
  • インスタンスを定義しているときにいくつか検索するポイントがある
  • データ型のコンパニオンオブジェクト
  • 型クラススーパークラスのコンパニオンオブジェクト
  • 型クラスのコンパニオンオブジェクト

NEWTYPE

ひとつの型に関して複数のインスタンスが定義されることがある

implicit val sumMonoid: Monoid[Int] =
  new Monoid[Int] {
    def zero: Int = 0
    def append(x: Int, y: Int): Int = x + y
  }

implicit val productMonoid: Monoid[Int] =
  new Monoid[Int] {
    def zero: Int = 1
    def append(x: Int, y: Int): Int = x * y
  }

importによる明示的なインスタンスの選択

値クラスを使ってインスタンスが一意にしてもよい

case class Sum(value: Int) extends AnyVal

case class Product(value: Int) extends AnyVal

implicit val sumMonoid: Monoid[Sum] =
  new Monoid[Sum] {
    def zero: Sum = Sum(0)
    def append(x: Sum, y: Sum): Sum = Sum(x.value + y.value)
  }

implicit val productMonoid: Monoid[Product] =
  new Monoid[Product] {
    def zero: Product = Product(1)
    def append(x: Product, y: Product): Product = Product(x.value * y.value)
  }

IntのMonoidを数値に一般化

implicit def sumMonoid[A](implicit A: scala.math.Numeric[A]): Monoid[A] =
  new Monoid[A] {
    def zero: A = A.zero
    def append(x: A, y: A): A = A.plus(x, y)
  }

implicit def productMonoid[A](implicit A: scala.math.Numeric[A]): Monoid[A] =
  new Monoid[A] {
    def zero: A = A.one
    def append(x: A, y: A): A = A.times(x, y)
  }

implictは階層構造をつくることで優先順位をつけることができる

trait LowPrioritySemigroupImplicits {
  implicit def numeric[A](implicit A: scala.math.Numeric[A]): Monoid[A] =
    new Monoid[A] {
      def zero: A = A.zero
      def append(x: A, y: A): A = A.plus(x, y)
    }
}

object Semigroup extends LowPrioritySemigroupImplicits {
  implicit def int: Monoid[Int] =
    new Monoid[Int] {
      def zero: Int = 0
      def append(x: Int, y: Int): Int = x + y
    }
}

higher-kinded type

  • 型パラメータをとる型パラメータを指定できる
  • 例: Functor

mapを使った例

import scala.language.higherKinds

trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

Functorは写像とよばれる

  • mapのように関数をカリー化されたパラメータにとることで型推論を効かせることができる
  • 型パラメータ[_]
implicit val listFunctor: Functor[List] =
  new Functor[List] {
    def map[A, B](fa: List[A])(f: A => B): List[B] = fa.map(f)
  }

implicit val optionFunctor: Functor[Option] =
  new Functor[Option] {
    def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa.map(f)
  }

Functorを使った実装例

def pair[F[_], A](fa: F[A])(implicit F: Functor[F]): F[(A, A)] = F.map(fa)(a => (a, a))
assert(pair(List(0, 1, 2)) == List((0, 0), (1, 1), (2, 2)))

assert(pair(Option(0)) == Some((0, 0)))
  • EitherもFunctorとみなせる
  • Eitherは型パラメータを2つ取る
implicit def eitherFunctor[A]: Functor[({ type F[B] = Either[A, B] })#F] =
  new Functor[({ type F[B] = Either[A, B] })#F] {
    def map[B, C](fa: Either[A, B])(f: B => C): Either[A, C] = fa.right.map(f)
  }
  • 型の部分適用の記法
  • type F[B]
  • Either[A, B]
  • type F[B] = Either[A, B]

この例だと型を指定する必要がある(型を指定しないとコンパイルエラー)

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

assert(pair(Right(0): StringEither[Int]) == Right((0, 0)))

まとめ

  • 型クラスを使いこなす
  • implicitを知る
  • Scalaの型推論を知る

俺的まとめ

  • Functor Monoid
  • implicit
  • コンパニオンオブジェクト

Scala関数型デザイン&プログラミング ―Scalazコントリビューターによる関数型徹底ガイド (impress top gear)

Scala関数型デザイン&プログラミング ―Scalazコントリビューターによる関数型徹底ガイド (impress top gear)