12
7

More than 1 year has passed since last update.

Cats の関数覚え書き(Functor, Apply, Applicative, FlatMap)

Last updated at Posted at 2021-11-30

この記事は MicroAd Advent Calendar 2021 の 1 日目の記事です。

はじめに

現在、弊チームでは Cats 製の WEB アプリを実装(既存のアプリのリプレイス)を進めています。

Cats にはたくさんの型クラスにたくさんの関数が定義されているのですが、それを知った上で実装を行うのと、知らないで実装を行うのではコーディングの難易度に天と地ほどの差があるように感じています。

今回はチームメンバーへの共有も兼ねて、よく使うであろう型クラスの関数について覚書程度ですが使い道などを書いていこうと思います。

  • flatMap とそれに連なる型クラス
    • Functor
    • Bifunctor
    • FunctorFilter
    • Apply
    • Applicative
    • FlatMap
  • エラー処理に関する型クラス
    • ApplicativeError
    • MonadError
  • cats.kernel 系
    • Eq
    • Order
    • Semigroup
      • (SemigroupK)
    • Monoid
      • (MonoidK)
  • traverse とそれに連なる型クラス
    • Foldable
    • Bifoldable
    • Reducible
    • Traverse
    • Bitraverse
    • TraverseFilter

上記の型クラスに定義されている関数を紹介しようと思ったのですが、めちゃくちゃ長くなってしまったので記事を分割して、1カテゴリーずつ毎週水曜日に紹介していこうと思います。

本記事では、

  • Functor
  • Bifunctor
  • FunctorFilter
  • Apply
  • Applicative
  • FlatMap

をそれぞれ見ていこうとおもいます。

今回使用する cats, scala のバージョン

  • scala 2.13.1
  • cats 2.6.1

使用するクラスについて

本記事では、関数の使い方の具体例を書いていこうと思うのですが、以下の様なクラスを使って例をあげていこうと思っています。

// 値クラス
case class AccountId(private val toInt: Int) extends AnyVal

case class AccountName(override val toString: String) extends AnyVal

// 値クラスを使ったクラス
case class Account(private val id: AccountId, private val name: AccountName)

// ファーストクラスコレクション
case class AccountIds(private val toList: List[AccountId]) extends AnyVal

case class Accounts(private val toNel: NonEmptyList[Account]) extends AnyVal

よくある感じですね。これらを使って実装例を示していきますので、覚えておいていただければと思います。

F として抽象化する際のコード例について

関数の説明をする中で、具体的な List, Either などのクラスとしてではなく、F として抽象化した状態での関数の使い方を説明する場合があります。

class FunctorContext[F[_]: Functor] {
  def f: F[Int => String] = ???
  def apply(i: Int): F[String] = ???
}

その時は、上記のようなクラスを作り、その中で対象の型クラスの関数を使う様にして説明していこうと思います。

関数の使い方の説明について

関数の使い方を説明する際、なるべく

  • 〇〇を使わないと・・・
  • 〇〇を使うと・・・

というように、関数を使わなかった場合の実装と、使った場合の実装を比較して説明してしようと思っています。

関数を使う例が List, Either などの scala 標準で用意されているデータ型の場合は、〇〇を使わないと・・・の方ではなるべく cats 自体を使わないように努め、cats を使う場合とそうでない場合の比較が出来るようにしようと思っています。

関数を使う例がF[_]: Functorなどとして抽象化してある場合は、cats を使わないのは無理なので、他の実装方法をとる場合はどういう風になるのか、例えばその関数の存在を知らない場合は、どういう実装が必要になってしまうのかの様な比較が出来る様に書いていこうと思います。

syntax をなるべく使う

cats では関数の実装(定義)とは別に、それをスマートに使うための syntax が用意されています。例えば、Foldable::combineAll という関数を使う場合、syntax を使わないで書くと、

Foldable[List].combineAll(List(1, 2, 3))
// 6

この様に書く必要がありますが、syntax を使って書くと

List(1, 2, 3).combineAll
// 6

この様に書くことが出来ます。こちらのほうが自然ですよね。

実際にコーディングをする場合はこちらを使うことになると思いますので、実践的な意味で syntax を使える場合は使ってコード例を示していこうと思います。


Functor

まずは Functor です、Functor は「map を使えるでお馴染みの」存在ですので、map を使った派生系の独自関数が定義されているのと、制約が弱いからか、いろんな場面で役に立ちそうな関数も一部実装されています。

widen

def widen[A, B >: A](fa: F[A]): F[B] = fa.asInstanceOf[F[B]]

「広くする」という名前が表す通り、B を継承している A の場合にF[A]F[B]として扱える様にしてくれる関数です。こちらは、実装を見ると map を使って実装されているものではない関数だというのがわかると思います。こちらは、普段 List, Option などの共変のクラスを使っている限りはあまり必要性を感じないかもしれないのですが、不変なクラスに対する処理として役に立つ場面がありましたので紹介します。まず enum などの継承関係のあるクラスがあるとします。

sealed trait Role

case object Father extends Role

case object Mother extends Role

case object Daughter extends Role

このクラスを不変な F で包んで返す場合に、コンパイルエラーが発生します。

def apply[F[_] : Monad]: F[Role] = Father.pure[F] // コンパイルエラー

不変な F の場合F[Father]F[Role]は同一な型とは見なさないからですね。

widen を使わないと・・・

この様なコンパイルエラーを回避する方法としては

  • そもそも F を共変にする
  • Father を Role 型として型変換を行う

など、いくつかあると考えられます。

// 共変にする
def apply[F[+_] : Monad]: F[Role] = Father.pure[F]
// 型変換1
def apply[F[_] : Monad]: F[Role] = (Father: Role).pure[F]
// 型変換2
def apply[F[_] : Monad]: F[Role] = Father.pure[F].map { role: Role => role }

こちらでも同じ目的は果たせるのですが、

  • F を共変にした場合
    • この関数を使うクラスの F も(その先のクラスも全て)共変にする必要がある
    • Id モナドなどの不変な型クラスが F として扱えなくなる
  • 明示的な型変換は少し冗長な記述になる

など、それぞれ少し辛い部分が出てきます。

widen を使うと・・・

その様な場合に widen を使うと不変の定義のままスッキリと書くことができます。

def apply[F[_] : Monad]: F[Role] = Father.pure[F].widen

便利ですね。今回のような型合わせしたくて困っているときは使ってみてください。

void

def void[A](fa: F[A]): F[Unit] = as(fa, ())

名前が示す通り、F[A]の中身を捨ててF[Unit]に変換してくれる関数です。

使いどころとしては、例えばバリデーション処理などで Either(や MonadError)の Right の中身自体には関心がない時、Either[Throwable, Unit]として返したいけれど、最後に合成する値がEither[Throwable, Unit]になっていない、という場面で効果的かなと感じています。以下の様な関数があるとして、

def f(str: String): Either[Throwable, Boolean] = ???
def g(bool: Boolean): Either[Throwable, String] = ???
def apply(str: String): Either[Throwable, Unit] = ???

f と g を使って apply を実装したいとします。

void を使わないと・・・

その際に、for 式で表現したり、map で Unit に変換したりすると少し冗長な記述になりますよね。

def apply(str: String): Either[Throwable, Unit] =
  for {
    r1 <- f(str)
    _ <- g(r1)
  } yield ()
def apply(str: String): Either[Throwable, Unit] =
  f(str).flatMap(g).map(_ => ())

void を使うと・・・

そんなときに使えるのが void です。

def apply(str: String): Either[Throwable, Unit] = f(str).flatMap(g).void

ちょっとしたことですが、シンプルに記述できて気持ちいいですね。F が Monad の時(F[_]: Monad)などに().pure[F]と書いている場合に、void を使った処理で書き直せるかも?と思い出してあげてください。

fproduct

def fproduct[A, B](fa: F[A])(f: A => B): F[(A, B)] = map(fa)(a => a -> f(a))

こちらは、map + map する前後の状態を tuple で持ってくれるという関数です。

def f(accountId: AccountId): Account = ???
val accountIds: List[AccountId] = ???
def apply: Map[AccountId, Account] = ???

例えば、AccountId を受け取って Account を返してくれる関数 f があるとして、それを使ってList[AccountId]からMap[AccountId, Account]を作るような関数 apply を実装したいとします。

fproduct を使わないと・・・

fproduct を使わない場合をまず考えてみると、map を使って tupple にしたものを toMap で Map に変換すると思います。

def apply: Map[AccountId, Account] = accountIds.map(id => id -> f(id)).toMap

こういう処理を書きたい場合、にまさに fproduct でシンプルに記述できるとのことです。

fproduct を使うと・・・

上記と等価なコードを fproduct を使って書くと、

def apply: Map[AccountId, Account] = accountIds.fproduct(f).toMap

の様になります。シンプルですね、ただ、実装をリアルに考えると、関数 f は大体の場合 Option であったり、Either などのコンテキストに包まれて返ってくることが多いと思います。その様な場合は、また別の関数が必要になるでしょう。一つの例としては次に紹介する tupleLeft があると思います。

tupleLeft

def tupleLeft[A, B](fa: F[A], b: B): F[(B, A)] = map(fa)(a => (b, a))

こちらはコンテキストに入った値を、渡した値によって、コンテキストに入ったタプルにしてくれる関数です。fproduct で使った例を少し変えて、

def f(accountId: AccountId): Option[Account] = ???
val accountIds: List[AccountId] = ???
def apply: Map[AccountId, Account] = ???

今度は f が Option に包まれた Account を返す場合を考えてみます。

tupleLeft を使わないと・・・

まず tupleLeft を使わないで実装することを考えてみると、

def apply: Map[AccountId, Account] =
  accountIds.flatMap(id => f(id).map(id -> _)).toMap

この様に flatMap で Option を潰して実装すると思います。

tupleLeft を使うと・・・

次に tupleLeft を使って実装すると、

def apply: Map[AccountId, Account] =
  accountIds.flatMap(id => f(id).tupleLeft(id)).toMap

一文字多くなっていますが、id -> _ という部分の意図が、明確になっていてほんの少し改善された気がします、結果の型が左右逆になっている tupleRight もあります。

蛇足ですが・・・

ちなみに、この処理は FlatMap::mproduct を使うと少しシンプルに書くことが出来ます。

def apply: Map[AccountId, Account] =
  accountIds.mproduct(f(_).toList).toMap

toList, toMap を書かなくてはいけないのが、少し野暮ったいですね、この例に限った話だと Kotlin の associateBy のような関数が欲しくなってしまいますが scala 自体が特定の具体的な型への変換する処理があまりない印象なので、そういった関数はあえて用意していない気がします。

as

def as[A, B](fa: F[A], b: B): F[B] = map(fa)(_ => b)

こちらは、void を 一般化して Unit 以外の結果を返せる様にした関数で、中身を捨てて、引数で渡した値がコンテキストに包まれて返ってくる様な処理になっています。List, Option などで考えた場合「中身捨てたら意味なくない・・・?」と感じると思うんですが、こちらを効果的に使える一例としては、何かを返す処理にログを差し込みたいケースがあります。

as を使わないと・・・

そもそも前提知識としては、cats では(log4cats を使っていると) log の出力も副作用として扱われる(F[Unit]という結果が返ってくる)ので、途中の処理としてただ呼び捨てればいいのではなくFを結果として返す必要があります。

先ほどの例を流用して、以下の account を探す処理に対して、存在しなかった場合は warn ログを出力する処理を追加したいとします。

def f(accountId: AccountId): Option[Account] = ???

こちらに 関数型っぽくない感覚でロギングを追加しようとすると・・・

val id: AccountId = ???
val maybeAccount: Option[Account] = f(id)
if (maybeAccount.isEmpty) {
  logger.warn(s"Not found account. accountId = $id")
}
maybeAccount

の様な処理を書こうとしてしまうと思うのですが、この場合、logger.warn の結果が捨てられてしまうので、ログが出力されません。ですので、例えば、以下の様に書いて logger.warn から返ってきた F を返してあげる必要があります。

val id: AccountId = ???
val maybeAccount = f(id)
logger
        .warn(s"Not found account. accountId = $id")
        .whenA(maybeAccount.isEmpty)
        .map(_ => maybeAccount)

whenA などを使っていい感じの書き方ができると思いますが、.map(_ => maybeAccount)の部分の処理が少し不明瞭に思えます。

as を使うと・・・

そんなときに as を使うと、処理を明確に記述することができます。

val maybeAccount = f(id)
logger
        .warn(s"Not found account. accountId = $id")
        .whenA(maybeAccount.isEmpty)
        .as(maybeAccount)

ちょっとした違いですが as、便利ですね。

ifF

def ifF[A](fb: F[Boolean])(ifTrue: => A, ifFalse: => A): F[A] =
  map(fb)(x => if (x) ifTrue else ifFalse)

こちらは、これまで紹介した関数とは違い、syntax としては定義されていない関数になります、syntax として定義されている場合、クラスの拡張関数として使用できるのですが、されていない場合は、Functor[F].ifF(_)(_, _)の様に書く必要があります。

さて、以下の様なクラスがあるとします。

class FunctorContext[F[_] : Functor] {
  private val boolF: F[Boolean] = ???

  def apply: F[String] = ???
}

apply の実装として、F[Boolean]の中身の Boolean によって処理を分岐して、true だったら、"秦"、false だったら"魏"と返すことをしたい場合を考えてみます。

ifF を使わないと・・・

ifF を使わないやり方だと、map を使ってパターンマッチか分岐を作る形で以下の様に実装すると思います。

def apply: F[String] = boolF.map {
  case true => "秦"
  case false => "魏"
}
def apply: F[String] = boolF.map(if (_) "秦" else "魏")

ifF を使うと・・・

ifF を使う場合は以下の様に実装します。

def apply: F[String] = Functor[F].ifF(boolF)("秦", "魏")

書き方としてはほんの少しの違いですが、自分で分岐を書かないでよくなるのは大きい違いかなと思います、・・・そうでもないですかね。

unzip

def unzip[A, B](fab: F[(A, B)]): (F[A], F[B]) =
  (map(fab)(_._1), map(fab)(_._2))

こちらは List などの標準関数として定義されているものを抽象化したものなので、見慣れていると思います、ですので List などの具象な型に対して使うというより、F[_]: Functorなど Functor の型クラスとして抽象化されている処理に対して 実装を行う場合に役に立つ関数かなと思っています。

unzip を使わないと・・・

unzip が Functor に対して使えることを知らないと、F に包まれたタプルに対する unzip な処理を書く場合、map(_._1) map(_._2)を自分で書く必要があると思います。

private val tupleF: F[(String, Int)] = ???
val apply: (F[String], F[Int]) = tupleF.map(_._1) -> tupleF.map(_._2)

unzip を使うと・・・

Functor に対して unzip が使えることを知っている場合、

val apply: (F[String], F[Int]) = Functor[F].unzip(tupleF)

と書くことができて、スッキリしますね。

mapApply

def mapApply(a: A): F[B] 

こちらは F[_]: Functorに包まれた Function1 の場合、つまりF[Function1[A, B]]のときに使える関数になります。その名の通り、map + apply をいっぺんにやってくれる様な処理が書けます。

class FunctorContext[F[_]: Functor] {
  def f: F[Int => String] = ???
  def apply(i: Int): F[String] = ???
}

例えば、F[_]: Functorに包まれたInt => Stringを返す関数 f を使って、引数として渡された Int を使ってF[String]を返す関数 apply を実装したい場合を考えてみます。

mapApply を使わないと・・・

mapApply を使わない実装を考えてみると、map + apply で実装できそうです。

def apply(i: Int): F[String] = f.map(_.apply(i))

もしくは apply を省略して。

def apply(i: Int): F[String] = f.map(_(i))

という風に書くでしょうか。

mapApply を使うと・・・

def apply(i: Int): F[String] = f.mapApply(i)

この様に書けます。ほんのちょびっとだけ、意図がわかりやすいコードになった気がしないでもないような気持ちになりますよね。

_1F

def _1F(implicit F: Functor[F]): F[A] 

こちらは、F[_]: Functorに包まれたタプル2(ペア)のとき、つまりF[(A, B)]のときに使える関数になります。

class FunctorContext[F[_]: Functor] {
  def f: F[(Int, String)] = ???
  def apply: F[Int] = ???
}

例えば、F[(Int, String)]の 1 つ目の要素を取得したい場合を考えてみます。

_1F を使わないと・・・

def apply: F[Int] = f.map(_._1)

map の中で_1をする感じの実装をすると思います。

_1F を使うと・・・

def apply: F[Int] = f._1F

これだけで済むようになります。ほんとに些細なことですが、シンプルなコードになりました。同様に 2 つ目の要素を取り出したい場合は_2Fという関数が用意されています。1 つ目と 2 つ目の要素を入れ替えたい場合はswapFという関数も用意されています。

Functor の関数についての感想

個人的には、widen, void, as などの関数は F としてクラスや関数を定義する上で、とても使い勝手がいいと感じました、fproduct, tupleLeft, tupleRight については、今のところあまり使わなそうだなと思っているのですが この関数があると知ることで、アイディアが増えて使いどころが見つかっていくかもしれないですよね。

Bifunctor

bi というとバイリンガルの bi と同じで「二つの状態を併せ持つ」という語感がありますね。こちらの型クラスのインスタンスとしては、Either, Tuple2, Ior, Validated などがあります。

bimap

def bimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): F[C, D]

Functor::map と同じ様に Bifunctor::bimap という関数があるのはわかりやすいですね。こちらは、右側と左側どちらに対しても map 処理を行いたい場合に使うことができます。Either, Tuple2 を使った例を見てみましょう。

1.asRight[String].bimap(new Throwable(_), _ * 100.0)
// Right(100.0)
"test".asLeft[Int].bimap(new Throwable(_), _ * 100.0)
// Left(java.lang.Throwable: test)
("test", 1).bimap(new Throwable(_), _ * 100.0)
// (java.lang.Throwable: test,100.0)

まぁこう動くだろうね、と想像した通りの結果になったんじゃないでしょうか。Either も Tuple2 も map を使うと右側しか処理できないため、どちらも同時に処理したい場合に使うと良さそうです。

leftMap

def leftMap[A, B, C](fab: F[A, B])(f: A => C): F[C, B] = bimap(fab)(f, identity)

こちらは、左側だけ map してくれる関数です。

1.asRight[String].leftMap(new Throwable(_))
// Right(1)
"test".asLeft[Int].leftMap(new Throwable(_))
// Left(java.lang.Throwable: test)
("test", 1).leftMap(new Throwable(_))
// (java.lang.Throwable: test,1)

こちらも想像通りの結果になったのではないでしょうか。Either の場合はleft.mapと記述していたコードがこの関数一つにまとまっただけ、といえばそれまでですが使う場面はあるかもしれません。

leftWiden

def leftWiden[A, B, AA >: A](fab: F[A, B]): F[AA, B] = fab.asInstanceOf[F[AA, B]]

Functor::widen の left バージョンですね。個人的には widen が欲しくなる場面が今のところ不変な F として抽象化した場合だけなのですがF[A, B]の様に抽象化するケースがないため使いどころがあまり思い浮かばないですが、欲しくなることもあるかもしれません。

Bifunctor の関数についての感想

Functor に対してはだいぶライトな概念でしたが、Either, Tuple2 の取り回しが多少楽になるかなと思います。この型クラスは Bifoldable, Bitraverse という Bi シリーズの型クラスの基になっており、それぞれあとでまた紹介しようと思います。

FunctorFilter

collect, filter, filterNot などのフィルタ系の処理も FunctorFilter として抽象化されています。標準関数では見ないような処理もあるので面白いです。

mapFilter

def mapFilter[A, B](fa: F[A])(f: A => Option[B]): F[B]

例えば AccountId の一覧に紐づく Account の一覧として取得したい場合、

def filterBy(accountIds: List[AccountId]): List[Account] = ???

このような関数を実装したい場合を考えてみましょう。

mapFilter を使わないと・・・

まずは mapFilter を使わない場合だと、はじめに浮かぶのは flatMap を使って、

def filterBy(accountIds: List[AccountId]): List[Account] =
  accountIds.flatMap(accounts.findBy)

というように、Option を潰して実装する方法だと思います。

mapFilter を使うと・・・

その様な場面で同じように使えるのが、mapFilter です。

def filterBy(accountIds: List[AccountId]): List[Account] =
  accountIds.mapFilter(accounts.findBy)

使う関数を変えただけで、ほとんど変化がないように思えますよね、Monad より弱い制約である FunctorFilter でも同じ表現力をもっていることがきっと良いのでしょう。

flattenOption

def flattenOption[A](fa: F[Option[A]]): F[A] = mapFilter(fa)(identity)

こちらは、その名の通りコンテキストの中身が Option で包まれているとき Option を flatten してくれる関数です、Kotlin の filterNotNull と同じ様な役割の関数と言えるかもしれません。

flattenOption を使わないと・・・

List の場合で考えると、List[Option[_]]は flatten を使えばList[_]と Option を潰してくれると思います。

List(1.some, none, 2.some).flatten

もしくは少し冗長ですが collect で Some の場合だけ中身を取り出して、

List(1.some, none, 2.some).collect { case Some(i) => i }

と書くでしょうか。

flattenOption を使うと・・・

flattenOption を使うと以下の様に書くことができます。

List(1.some, none, 2.some).flattenOption

こちらも mapFilter と同じように「flatten 使うのとほぼ一緒じゃん。むしろ関数名の分、長くなってるじゃん」と感じますよね。

FunctorFilter の関数についての感想

scala を使う上では、List も Option も IterableOnce を継承しているのでList[Option[A]]List[A]にする系の処理を書くときは、flatMap, flatten を使っておけばなんとでもなる感じがしますが、なんでもかんでも flatMap や flatten を使って力技で解決するより、より目的に沿った mapFilter, flattenOption を使うことで処理が明確になる。という考え方もあるかもしれません。

また、使用する型の組み合わせによっては、flatMap, flatten では Option を潰すことが出来ないケースがあるかと思いますので、そんなときはこれらの関数がいた事をいつか思い出してあげてください。なお、collect, filter, filterNot などの見慣れた関数や、flattenOption は mapFilter を使って再実装されているため、FunctorFilter の型クラスのインスタンスにしたい場合、すでに Functor のインスタンスであれば mapFilter を定義してあげれば良いようです。


Apply

def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]

続いては ap が使えるやつでお馴染みの Applyです、こちらも Functor にとっての map と同じ様に ap を使って実装された便利な関数がいくつかありますので紹介させてください。

productR, *>

def productR[A, B](fa: F[A])(fb: F[B]): F[B] =
  ap(map(fa)(_ => (b: B) => b))(fb)

まずは、productR とそのエイリアスである*>です。定義をみるとわかりますが、fa の中身を使うことなく捨てられて、fb の中身に置き換わります。こちらも List, Option で考えると使いみちがよくわからないと思うのですが、Either で考えると意味がわかりやすいかもしれないなと思っています。以下のように f, g の2つの結果を合成して返り値としては g の中身の Boolean にしたい場合を考えてみます。

def f: Either[Throwable, String] = ???
def g: Either[Throwable, Boolean] = ???
def apply: Either[Throwable, Boolean] = ???

productR を使わないと・・・

productR を使わないで愚直に実装すると、

def apply: Either[Throwable, Boolean] = f.flatMap(_ => g)

と書くと思います。f が Left の場合は、f のエラーを、Right の場合は中身が使われないで、g の結果を返す様な処理になっていると思います。

productR を使うと・・・

flatMap で実装をしてもいいんですが、_ => gの部分が少し嫌ですよね?そんなときは productR を使います。

def apply: Either[Throwable, Boolean] = f *> g

等価なコードをこのようにシンプルに書くことが出来ました。F に対する処理として考える場合、例えば、ログを出したあとに何かを評価して返したいというケースを考えてみます。

class ApplyContext[F[_] : Apply](logger: Logger[F]) {

  private def f: F[String] = ???

  def apply: F[String] = logger.warn("ワンワンニャンニャン") *> f

}

Functor で説明した通り、ログを出力するためには F を継続しなくてはいけません。何かしらの方法で logger.warn の結果であるF[Unit]の F を返す必要があるので、こういう感じで書く場面も出てくるかなと思います。Apply は Monad よりも制約が弱いため、flatMap を使えない様なデータ型に対しても(例えば Validated 型など)Apply の型クラスのインスタンスならばこの関数を使える、という点も良いかと思います。

productL, <*

def productL[A, B](fa: F[A])(fb: F[B]): F[A] = map2(fa, fb)((a, _) => a)

productR と対になるような、productL とそのエイリアスである<*です。productR の例をなぞって考えてみます、先ほどの例とほとんど一緒ですが、apply の型が Either[Throwable, String]になっています。

private def f: Either[Throwable, String] = ???
private def g: Either[Throwable, Boolean] = ???
def apply: Either[Throwable, String] = ???

このとき、処理順は変えずに(f -> g の順番で)結果だけ f の結果を返すように実装したいとします。

productL を使わないと・・・

productL を使わないで実装すると flatMap だと少し冗長になるので for 式を使って書くんじゃないかなと思っています。

def apply: Either[Throwable, String] =
  for {
    str <- f
    _ <- g
  } yield str

productL を使うと・・・

def apply: Either[Throwable, String] = f <* g

シンプルですね!productL は実行する順番は productR と同じく左->右になりますが、返す結果は左側のものになります。この様な処理を書きたくなったとき思い出してあげてください。

product

override def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] =
  ap(map(fa)(a => (b: B) => (a, b)))(fb)

こちらは実際には Apply ではなく、Semigroupal として抽象化されている関数ですが、Apply は Semigroupal でもあるので、product を使用することができます(override されています)ので紹介します。二つの F に包まれた値を F に包まれた Tuple として返してあげる様な関数ですね。注意点としては、こちらは syntax として定義されていない関数になるので、

Apply[F].product(???, ???)

という風に呼び出す必要があります。

val maybeInt: Option[Int] = ???
val maybeString: Option[String] = ???
def apply: Option[(Int, String)] = ???

今回は、Option[Int]Option[String]がそれぞれ Some だった場合に、Option[(Int, String)] にして、そうでない場合な None にしたい場合を考えてみましょう。

product を使わないと・・・

そういう処理を書く場合は、for 式か flatMap を使って以下の様に書くと思います。

def apply: Option[(Int, String)] =
  for {
    i <- maybeInt
    str <- maybeString
  } yield i -> str
def apply: Option[(Int, String)] =
  maybeInt.flatMap(i => maybeString.map(i -> _))

product を使うと・・・

product を使う場合

def apply: Option[(Int, String)] =
  Apply[Option].product(maybeInt, maybeString)

こういう風に書くことができます。こちらも flatMap を使わないで実装できる点がよいのだと思います。が、ピンポイントで Tuple にしたい場合ってそんなにないですよね・・・? そんなときは、product を使いやすくした(一般化した様な) map2 という関数があるので次に紹介します。product 自体は、map2 や ap2 などの関数の下地になっているので大事な存在ではあると思います。

map2

def map2[A, B, Z](fa: F[A], fb: F[B])(f: (A, B) => Z): F[Z] =
  map(product(fa, fb))(f.tupled)

map に 2 がついているこの map2。個人的には使うシーンが多くて好きな関数です。例えば、Option の2つの値どちらも存在した場合だけ処理を書きたい場合、今回はOption[Int]が2つとも Some だった場合に掛け算結果を返し、それ以外は None になる関数を実装してみるとしましょう。

val maybeInt1: Option[Int] = ???
val maybeInt2: Option[Int] = ???
def apply: Option[Int] = ???

この apply 関数を実装します。

map2 を使わないと・・・

map2 を使わない場合、普通に書くと flatMap か for 式を使って以下の様に実装しますよね。

def apply: Option[Int] = maybeInt1.flatMap(i => maybeInt2.map(i * _))
def apply: Option[Int] =
  for {
    i <- maybeInt1
    j <- maybeInt2
  } yield i * j

よくある感じですね。

map2 を使うと・・・

ここで map2 を使うと、

def apply: Option[Int] = maybeInt1.map2(maybeInt2)(_ * _)

この様にシンプルに書くことができます。map2 意外にも map3, map4... mapN という関数が用意されているので、より複雑なケースに対してもシンプルに実装できる様になっています。

map2Eval

def map2Eval[A, B, Z](fa: F[A], fb: Eval[F[B]])(f: (A, B) => Z): Eval[F[Z]] =
  fb.map(fb => map2(fa, fb)(f))

名前の通り map2 の Eval 版 であるこの関数です。先ほどの例と同じ様な処理の場合で、maybeInt2 の処理がものすごく重いときに値の生成を遅らせたいシーンがあると思います。「maybeInt1 が None だった場合は maybeInt2 を評価しない様にしたい」ということを叶えてくれるのが、この map2Eval です。

map2Eval を使わないと・・・

先ほどの例だと「maybeInt2 を def にすれば済むんじゃないの?」という感じだと思います。

val maybeInt1: Option[Int] = ???
def maybeInt2: Option[Int] = ??? // めちゃ重い処理
def apply: Option[Int] = maybeInt1.flatMap(i => maybeInt2.map(i * _))

これでも実現できるんですが、cats には Eval という正格性を表すクラスが定義されており、評価のタイミングをコントロールしやすい設計ができる様になっているんですね。

map2Eval を使うと・・・

使う型自体が変わってしまったのですが、maybeInt2 を Eval.later などで非正格な状態として持っている場合、以下の様に実装することができます。

val maybeInt1: Option[Int] = ???
val maybeInt2: Eval[Option[Int]] = ??? // めちゃ重い処理
def apply: Option[Int] = maybeInt1.map2Eval(maybeInt2)(_ * _).value

今回の例だけみると大したメリットに感じないと思うのですが、Eval な値でも同じ様に実装ができることで設計の幅が広がってとても良いのではないかと思っています。

ap2

def ap2[A, B, Z](ff: F[(A, B) => Z])(fa: F[A], fb: F[B]): F[Z] =
  map(product(fa, product(fb, ff))) { case (a, (b, f)) => f(a, b) }

定義を見た時点で複雑な関数だなぁと思ってしまいますが、見ての通りコンテキストに包まれた「タプルを受け取って変換する関数」に、コンテキストに包まれた二つの値を渡すと、コンテキストに包まれた変換後の値が返ってくる関数ですね。具体例を考えてみましょう。

val maybeDurationOf: Option[(Long, TimeUnit) => Duration] = Some(
  FiniteDuration.apply
)
val maybeLong: Option[Long] = 1L.some
val maybeTimeUnit: Option[TimeUnit] = TimeUnit.DAYS.some
def apply: Option[Duration] = ???

上記の様に、Option に包まれた Duration の生成関数と、Option に包まれた、Long, TimeUnit があるとします。

ap2 を使わないと・・・

ap2 を使わずに実装するとどうなるかというと

def apply: Option[Duration] = for {
  durationOf <- maybeDurationOf
  long <- maybeLong
  timeUnit <- maybeTimeUnit
} yield durationOf(long, timeUnit)

この様に、for 式を使って実装するかと思います。

ap2 を使うと・・・

def apply: Option[Duration] = maybeDurationOf.ap2(maybeLong, maybeTimeUnit)

この様に書くことが出来ます。Option は Apply であり、Monad でもあるのでたまたま for 式が使えますが、for 式が使えないクラスの場合でも ap2 を使って同じ様なことが実現できるのがすごいですね。

ifA

def ifA[A](fcond: F[Boolean])(ifTrue: F[A], ifFalse: F[A]): F[A] = {
  def ite(b: Boolean)(ifTrue: A, ifFalse: A) = if (b) ifTrue else ifFalse

  ap2(map(fcond)(ite))(ifTrue, ifFalse)
}

Functor にも同じ様なのあったな?と感じますよね。

def ifF[A](fb: F[Boolean])(ifTrue: => A, ifFalse: => A): F[A]

Functor::ifF は、Apply::ifA と比べてみるとわかる通り、

  • 引数がコンテキストに包まれていない
  • 非正格である

という違いがあります。何やら ap2 やインナーな関数などを駆使して複雑なことをやって実現していそうです。さて、以下の様な関数があったとして apply 関数を、boolF の中身が true だった場合は f を、そうでない場合は g を評価する様な関数として実装する場合を考えましょう。

class ApplyContext[F[_] : Apply] {
  private val boolF: F[Boolean] = ???

  private def f: F[String] = ???

  private def g: F[String] = ???

  def apply: F[String] = ???
}

ifA を使わないと・・・

ifA を使わない場合はどういう風に実装するでしょうか、仮に F が for 式や flatMap を使えるとすると、以下の様に実装すると思います。

def apply: F[String] =
  for {
    b <- boolF
    result <- if (b) f else g
  } yield result
def apply: F[String] = boolF.flatMap(if (_) f else g)

実際は、for 式、flatMap は使えませんので、どういう風に実装するかというと・・・難しいので、ifA の定義から逆算して考えると、

def apply: F[String] =
  boolF.map(b => (s1: String, s2: String) => if (b) s1 else s2).ap2(f, g)

こういう実装になると思います。大変ですね、まずF[Boolean]F[(String, String => String)]に変換してから、ap2 を使う様に実装すると実現できるみたいです。ap2 すごい。。

ifA を使うと

ifA を使う様に変更すると、以下の様になります。

def apply: F[String] = boolF.ifA(f, g)

ifF とは違い、syntax にある関数なので、boolF に直接生やすことができました。シンプルで良いですね。

Apply の関数についての感想

FunctorFilter もそうでしたが Apply で定義されている関数は、List, Option, Either などの標準的なクラスを使っている限りは for 式か flatMap が使って同じ様な処理を書けるものがほとんどだったと思います。ですが、flatMap を使ってなんでもかんでも書くよりも、処理が明確に記述できたり単純にシンプルにかけたりすることがわかりました。

Monad ではない Apply の型クラスのインスタンスの場合や、F[_]: Applyとして抽象化されている場合は そもそも flatMap は使えないと思うので、そういった場面で使い勝手がいい関数がたくさんありましたね。


Applicative

def pure[A](x: A): F[A]

pure が使えるでお馴染みの Applicative ですが Functor にとっての map, Apply にとっての ap と同様に、pure によって実装された便利な関数が定義されています。

unit

def unit: F[Unit] = pure(())

コンテキストに包まれた(pure な)Unit が返る関数ですね。

Applicative[List].unit
// List(())
Applicative[Option].unit
// Some(())

主に F として抽象化している場合に、条件分岐の返り値で使う場面など、あると思います、こちらを使って whenA, unlessA などが実装されています。

whenA

def whenA[A](cond: Boolean)(f: => F[A]): F[Unit] =
  if (cond) void(f) else unit

こちらも個人的には使いやすい関数です。Functor の時にチラッと例として書いてしまいましたが、条件によってコンテキストに包まれた非正格な値を評価するのかコンテキストに包まれた Unit を返すのかが変わる様な関数です。Option::when を一般化した様な関数になるんでしょうか。Option::unless と同様に unlessA という関数も whenA の判定が逆バージョンとして存在します。

whenA を使わないと・・・

whenA を使わない場合は、if 文を使って、else は void を返す様に書くと思います。

val accountId: AccountId = ???
def maybeAccount: Option[Account] = ???
if (maybeAccount.isEmpty) {
  logger.warn(s"Not found account. accountId = $accountId")
} else {
  Applicative[F].unit
}

この様な感じですね。

whenA を使うと・・・

whenA を使った場合は、どういう処理になるかというと、

val accountId: AccountId = ???
def maybeAccount: Option[Account] = ???
logger
  .warn(s"Not found account. accountId = $accountId")
  .whenA(maybeAccount.isEmpty)

この様にシンプルに書くことができます。条件が逆になっている unlessA という関数もあります。

replicateA

def replicateA[A](n: Int, fa: F[A]): F[List[A]] =
  Traverse[List].sequence(List.fill(n)(fa))(this)

インターフェイスだけ見ると、一見 map + List::fill でも実装できそうな関数じゃないかと感じるんですが、名前に A がついているので、Applicative に対して replicate を行う様な処理になっており、F[A]を List::fill した後で sequence する処理になっていますね。Option と Either の場合に、map で書いた場合と replicateA で書いた場合に結果の差はありません。

1.some.map(List.fill(3)(_))
// Some(List(1, 1, 1))
1.some.replicateA(3)
// Some(List(1, 1, 1))
none[Int].map(List.fill(3)(_))
// None
none[Int].replicateA(3)
// None
"ok".asRight[Throwable].map(List.fill(3)(_))
// Right(List(ok, ok, ok))
"ok".asRight[Throwable].replicateA(3)
// Right(List(ok, ok, ok))
"error".asLeft[Int].map(List.fill(3)(_))
// Left(error)
"error".asLeft[Int].replicateA(3)
// Left(error)

ですが、replicateA の docs に書いてある例通り、State を使った処理に関して違いが生まれます。

type Counter[A] = State[Int, A]
val getAndIncrement: Counter[Int] = State { i => (i + 1, i) }
val getAndIncrement5: Counter[List[Int]] =
  Applicative[Counter].replicateA(5, getAndIncrement)
getAndIncrement.map(List.fill(5)(_)).run(0).value
// (1,List(0, 0, 0, 0, 0))
getAndIncrement5.run(0).value
// (5,List(0, 1, 2, 3, 4))

コンテキストの中身を List にしたものと、コンテキストを sequence でまとめあげたものだとこの様な違いが出るんですね。

Applicative の関数についての感想

pure, void, whenA, unlessA はそれぞれ用途がわかりやすくて理解も難しくなかったですよね。すごく使いやすい関数だと思いました。


FlatMap

名前そのものですが、flatMap とそれを使って実装された関数を使える型クラスですね。FlatMap(flatMap) + Applicative(pure) = Monadという継承関係になっているので、F に対して flatMap は使うけど、pure は使わないなぁというときは Monad ではなく FlatMap を context bound してあげればよいのでしょうか。

def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]

このクラスも他と同じようにたくさんの面白い関数に出会うことが出来ます。

productREval, productLEval

def productREval[A, B](fa: F[A])(fb: Eval[F[B]]): F[B] =
  flatMap(fa)(_ => fb.value)
def productLEval[A, B](fa: F[A])(fb: Eval[F[B]]): F[A] =
  flatMap(fa)(a => map(fb.value)(_ => a))

Apply で登場した productR, productL の Eval 版ですね、以下は、productR, productL の定義です。

def productR[A, B](fa: F[A])(fb: F[B]): F[B] =
  ap(map(fa)(_ => (b: B) => b))(fb)
def productL[A, B](fa: F[A])(fb: F[B]): F[A] =
  map2(fa, fb)((a, _) => a)

インターフェイスとしてはいずれも、2つ目の引数が Eval で包まれているだけの違いしかないですね。Eval になっていると、ap では実装できないのか flatMap をつかって実装されていることがわかります。この違いは面白いですね。使用感は productR, productL とそれほど変わらないと思うので省略します。

mproduct

def mproduct[A, B](fa: F[A])(f: A => F[B]): F[(A, B)] =
  flatMap(fa)(a => map(f(a))((a, _)))

Functor::product と同じ様な名前のこの関数ですが、インターフェイスも受け取る関数の結果がコンテキストに包まれているかどうかだけ違う関数のようです。以下は product の実装になります。

def fproduct[A, B](fa: F[A])(f: A => B): F[(A, B)] = map(fa)(a => a -> f(a))

flatMap のインターフェイスとも似ている気がしませんでしょうか。

def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]

flatMap を使っているときに、結果を元の値の中身とのタプルとして持ちたい場合に使うことができそうです。例えばF[Int]Intを受け取ってF[String]を返す関数」を使って結果をF[(Int, String)]にする関数を実装することを考えてみます。

class FlatMapContext[F[_] : FlatMap] {
  val int: F[Int] = ???

  def f(i: Int): F[String] = ???

  def apply: F[(Int, String)] = ???
}

こちらの apply 関数を実装してみます。

mproduct を使わないと・・・

flatMap と for 式を使って実装すると以下のようになると思います。

def apply: F[(Int, String)] = int.flatMap(i => f(i).map(i -> _))
def apply: F[(Int, String)] =
  for {
    i <- int
    str <- f(i)
  } yield i -> str

ちょっと複雑なコードになっている感じがしますね。

mproduct を使うと・・・

mproduct を使えることを知っていれば、

def apply: F[(Int, String)] = int.mproduct(f)

このようにシンプルに実装することが出来ます。あまり使う場面のイメージはついていないですが、スッキリ書けて良いですね。

ifM

def ifM[B](fa: F[Boolean])(ifTrue: => F[B], ifFalse: => F[B]): F[B] =
  flatMap(fa)(if (_) ifTrue else ifFalse)

Functor::ifF, Apply::ifA などと同じシリーズっぽい名前のこちらの関数ですが、それらとの違いは何でしょうか。

def ifF[A](fb: F[Boolean])(ifTrue: => A, ifFalse: => A): F[A] =
  map(fb)(x => if (x) ifTrue else ifFalse)

Functor::ifF と比較をすると、受け取る2つの関数がコンテキストに包まれているかどうかの違いがあると思います。非正格な値として受け取るところは同じですね。

def ifA[A](fcond: F[Boolean])(ifTrue: F[A], ifFalse: F[A]): F[A] = {
  def ite(b: Boolean)(ifTrue: A, ifFalse: A) = if (b) ifTrue else ifFalse

  ap2(map(fcond)(ite))(ifTrue, ifFalse)
}

Apply::ifA と比較すると、受け取る2つの関数がコンテキストに包まれているところは同じですが、非正格な値として受け取るかどうかだけが違いそうです。これは興味深い違いですね。ifA の方は2つのF[A]を持ち上げてF[(A, A) => A]にする際にF[A]を評価する必要があるので、非正格な値として定義できないのでしょうか。結果をまだ計算していない場面で、ifM を使うことができる状況なら ifA ではなく ifM を使っておくほうが良さそうだなと言うことがわかります。

flatTap

def flatTap[A, B](fa: F[A])(f: A => F[B]): F[A] =
  flatMap(fa)(a => as(f(a), a))

flatMap と名前が似すぎていてややこしい感じがしますが、こちらの関数、tap という単語がついているので、何か中間の副作用っぽい処理をしてくれるのかなと予想がつくと思います。インターフェイス(と実装)を見ると、F[A]の中身の A を使ってF[B]を作る関数を受け取るんですが返る値としてF[A]を返すという作りになっていますね。scala 標準でも tap はscala.util.chaining._を import すれば使えましたよね。以下が定義になります。

def tap[U](f: A => U): A = {
  f(self)
  self
}

副作用を起こしたあと、自分自身を返すような関数になっています。cats では副作用を F として扱うので、この tap は使わずに flatTap を使うことになると思います。使い方としては、productL(<*)と同じ様な場面で使うのかなと思います。

productL との比較

まず productL(<*)を復習すると・・・

private def f: Either[Throwable, String] = ???
private def g: Either[Throwable, Boolean] = ???
def apply: Either[Throwable, String] = f <* g

f, g の順番で評価を行い、f が Left だった場合は g を評価せずに f の Left を返し、g が Left だった場合は g の Left を返し f と g がどちらも Right だった場合は f の Right を返す。ということを実現できる様な関数でした。これは f と g が直接は関係ない場合に使うことができるのですが、例えば g が f の Right を受け取りたいケースには対応できませんよね?

private def f: Either[Throwable, String] = ???
private def g(str: String): Either[Throwable, Boolean] = ???
def apply: Either[Throwable, String] = ???

flatTap を使わないと・・・

productL では実現できそうにないので、他の実装を検討してみると、、

def apply: Either[Throwable, String] =
  for {
    str <- f
    _ <- g(str)
  } yield str
def apply: Either[Throwable, String] =
  f.flatMap(str => g(str).as(str))

というように、for 式や flatMap を使って書いていくと思います。

flatTap を使うと・・・

こういう処理を書きたい場合は、flatTap を使うとシンプルに書くことが出来ます。

def apply: Either[Throwable, String] = f.flatTap(g)

便利ですね。

>>

def >>[B](fb: => F[B])(implicit F: FlatMap[F]): F[B]

*>(productR)と似ているこの関数ですが、インターフェイスもだいたい一緒です。

def productR[A, B](fa: F[A])(fb: F[B]): F[B]

比較すると、fb を非正格に受け取るようになっているのがわかると思います。この差が役に立つ場合は>>を使っていきたいですね。

FlatMap の関数についての感想

flatMap を使って実装されている関数は、これまでの型クラスで実装されてきたものと「似ているけど微妙に違うもの」が多かったように思います。これらの関数を知っておくことで更にかゆいところに手が届くようになったのではないでしょうか。

おわりに

Functor から始まり Apply, Applicative, FlatMap と Monad 配下の型クラスについて関数の使い方を見ていきました。

scala の標準で用意されている、List, Option, Either などのクラスはこれらのインスタンスになっていますし、IO ももちろんこれらのインスタンスになっているのもあり、コーディングの幅が大きく広がったように思います。

明日のアドベントカレンダーは

明日のアドベントカレンダーは @tobita_yoshiki さんによる「JavaからCを呼び出す」です。乞うご期待!

12
7
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
12
7