Scala
cats
ulgeekDay 7

Cats 入門 その3 (Cats のデータ型)

Cats 入門その3 (Cats のデータ型)

Cats と、 Cats の解説 PDF 「Advanced Scala with Cats」の紹介その3です。

その2 は こちら

今までは、 scala 標準のクラス (List, Option, Future, Either など) に付随している型クラスを紹介してきました。

今回は Cats が提供するデータ型を中心に紹介します。
主に Haskell で提供される機能を Scala に移植したものです。
これらを使うと本格的に FP の世界に入ってきてプログラムの見た目もかなり変わりますし、
設計なども大きく異なってきます。

それに、実際のアプリケーションを作るならもっと高度な FP のライブラリが必要になるはずです。

FP の知識はスケールしづらく、有識者が一人いても初心者に理解させるのは難しいと聞いたことがあります。

ここから先の機能を使うなら、ちゃんとコストをかける覚悟が必要になります。

Id モナド

Id モナドは普通の値を何もしないモナド値に見せかける仕組みです。

Cats によりモナドの性質を持つ型を Monad インスタンスとして定義できるようになるので、
モナド値を引数や戻り値・型パラメータに取るような関数も作れるようになります。

例えば次のようなモナド値を引数に取る関数を作るとロジックと実行時に与える Monad インスタンスを分けることができるので、場合によっては便利です。

Monad インスタンスを扱う例として、 Slick というDB操作をモナドで表現するライブラリがあるのですがこれを Monad インスタンスとして抽象化することで、UT では DB を扱わずに別のモナドで行うというある種モックのようなことが実現できました。
https://github.com/kencharos/slick_logic_within
Slick 版 https://github.com/kencharos/slick_logic_within/blob/master/src/test/scala/LogicWithinSuite.scala#L15-L24

モック版 https://github.com/kencharos/slick_logic_within/blob/master/src/test/scala/MockTestSuite.scala#L16-L48

def monadFunc[M[_]:Monad](m1:M[Int], m2:M[Int]):M[Int] = 
    for (a <- m1; b <- m2) yield (a + b)
}

monadFunc(1.some, 2.some) // Some(3)
monadFunc(List(1,-1), List(2,3)) // List(3, 4, 1, 2)

純粋に上記の関数のロジックのみをテストしたい場合、モナドの作用( Option の場合 None に代わりうる・List
の場合要素が増減するなど)を起こさないモナド値がほしくなります。

case class Box[A](value:A) のようなクラスを定義して、 Monad インスタンスを作ってしまいたくなりますが、より簡単にそれを行う方法として Id モナドが使えます。

Id モナドは 型パラメータ A に対する型エイリアスです。
次のように値に明示的に Id[A] を付与して生成します。
型エイリアスであるため Id[A] は実際には A と等しい値です。
その一方で Id[A] は Monad インスタンスであるため、ただの値ですがモナド値として扱うことが可能です。

// type Id[A] = A
import cats.Id

val id3:Id[Int] = 3 // cats.Id[Int] = 3
val id4 = 4:Id[Int] // cats.Id[Int] = 4 

id3 == 3 // true. Id[Int] は実際は Int に等しい 

monadFunc(id3, id4) // 7

Id モナドを使うことで、ただの値もモナド値として使えます。
この後出てくる Reader モナドなども内部的に Id を使っていたりします。

State モナド

変数の再代入や状態変更が許されない純粋な FP で状態をうまく扱うのは結構面倒です。

例えばスタック構造の push, pop を扱う場合は次のように
関数の引数にスタックを渡し、戻り値に変更後のスタックと結果のペアを得るようにしないといけません。

def push[A](a:A, stack:List[A]):(List[A], A) = (a::stack, a)

def pop[A](stack:List[A]):(List[A], A) = stack match {case (a::tail) => (tail, a)}

上の2つを使って push 2回のあと pop する処理を書くと次のようになります。

def push2ThenPop[A](a1:A, a2:A, stack:List[A]):(List[A], A) = {
    val (s1, _) = push(a1, stack)
    val (s2, _) = push(a2, s1)
    val (s3, res) = pop(s2)
    (s3, res)
}

push2ThenPop(3,4, List[Int]()) // (List(3), 4)

前の関数の呼び出し結果を次の関数の引数に渡していくあたりが、いかにも煩雑でバグを生み出しそうです。

このような状態変化をうまく扱うのが State モナドです。

私は State モナドの理解に苦労しました。
今も使いこなせるとは思っていませんが、理解するにあたって苦しんだのは次の点です。

  1. State が内部に保持する値は関数であり State モナドの計算の結果生み出されるのもまた関数である
  2. そもそも Scala だと State モナドを使わなくても似たようなことはできる

特に1点目が今までの Option や List など値を扱うモナドとは決定的に異なる点になります。

State モナドは S, A 2つの型パラメータを取り State.apply で、 S => (S,A) の関数を受け取ります。
S は状態変化を行っていくステートの型で、 A はステートを使用しつつ最終的に得る計算結果の型です。
State モナドは map, flatMap などの関数を使って S => (S, A) の関数をどんどん拡張していきます。

State モナド内に保持している関数は最終的に run メソッドに初期のステートを与えて実行します。

// 初期状態0で、Stateを生成
def st:State[List[Int], Int] = State(initialState => (initialState, 0))
// 初期状態の値を 1に変更
def st2 = st.map(_ + 1)
// 現在のステートに 初期値Aを追加し、Aをインクリメントする処理を追加
def st3 = st2.flatMap(a => State{currentState => (a::currentState, a + 1)})
// run 初期ステートを、空のリストにして実行
st3.run(List[Int]()).value // (List(1), 2)

こうなるのは State の flatMap が前の関数を実行した結果を次の関数に渡すという大きな関数を作成するためです。

具体的には次のようなコードになります。

def flatMap[M[_], A, B](m:State[S,A])(f:A => State[S,B]):State[S,B] = State{s => { //StateのflatMapは関数を作る。 
        val (_s, a) = m.run(s) // 前の関数を実行してステートを更新し、
        val (__s, b) == f(a)(_s)  // 引数の関数に、更新後のステートを与え、
        (__s, b) // 最後のステートと戻り値を、最終的な戻り値にする。
    }
}

これは実際の Cats のコードではないですが flatMap の逐次計算を行うという性質が、関数を値に使う場合でも出ていると思います。

State のように関数を連結して最終的に run で実行するというのは、 State に限らず関数を扱うモナドではよく出てくるパターンです。
(Monad で DB を扱う Slick も SQL 操作を関数のモナドで連結して最後にまとめてトランザクションで実行するというようなスタイルになりますね)

冒頭の push, pop を State でやり直すと次のように State を返す関数になります。

// 別名をつける。
type Stack[A] = State[List[A], A]

def pushS[A](a:A):Stack[A] = State{ state => (a::state, a)}
def popS[A]():Stack[A] = State{
    state => state match {case (a::tail) => (tail, a)}
}

ちなみに型パラメータを複数取るモナド値を簡単に扱うようにするため type で型パラメータを減らすのもよく使うパターンです。type を使うのはほかにも理由があるのですが、これは Monad Transformer の節であらためて解説します。

小さい粒度の State を返す関数を作ることで次のように for 式で連結ができます。
最初の State を使わない方と比べるとステートの受け渡しを行う必要がなくなり、だいぶすっきりしたと思います。

def push2ThenPopS[A](a1:A, a2:A):Stack[A] = {
    for(_ <- pushS(a1); _ <- pushS(a2); res <- popS())yield res
}

push2ThenPopS(3,4).run(List[Int]()).value // (List(3), 4)

Reader モナド

Reader は単純に言えば読み取り専用の State のようなものです。
グローバル変数のようなどの関数からでも参照したい情報を、
関数の引数にせずにモナドで引き回すようなプログラムができます。

サンプルとして利用できるOS の一覧とOS ごとの必要メモリを持つクラスを定義し、これを Reader モナドで引き回してみます。

case class Config(os:List[String], memory:Map[String, Int])

やりたいことはマシンの OS とメモリサイズを与えると、それがスペックを満たすかを調べることです。

まずは OS の取得・メモリサイズの取得それぞれを行うヘルパー関数を用意します。
State の時のように関数の戻り値をモナド値 (今回は Reader) にします。

Reader インスタンスは Config => A という Config から何かの値を取得するという関数を与えて生成します。

// タイプのエイリアスを作る
type ConfigReader[A] = Reader[Config,A]
// OS名を与えると、Configから当該OSが存在するかを調べる
def getSupportedOS(name:String):ConfigReader[Option[String]] = Reader{m => m.os.find(_ == name)}
// OS名を与えると、ConfigからOSごとの最低メモリサイズを取得する
def getMemorySizeForOS(os:String):ConfigReader[Option[Int]] = Reader{m => m.memory.get(os)}

Reader はモナドなので for 式で合成できます。
最終的にやりたいことは次のようなメソッドで実現できます。

def checkSpec(os:String, memory:Int):ConfigReader[Either[String, Boolean]] ={
    for ( _os <- getSupportedOS(os); // OSを取得
        _memory <- getMemorySizeForOS(os) //OSのメモリサイズを取得
    )yield {
        (_os, _memory) match { // 取得した値でチェック
            case (Some(_), Some(size)) if size < memory => true.asRight[String] //OK 
            case (Some(_), Some(size)) => s"${size} GB memory needs in ${os}".asLeft[Boolean] // メモリ不足
            case _ => s"${os} is not supported".asLeft[Boolean] // サポート外のOS
        }
    }
}

State 同様 run メソッドで実行を開始します。

// Reader に与える設定の内容
val conf = Config(List("Windows", "Linux"), Map("Windows" -> 3, "Linux" -> 2))

checkSpec("Windows", 4).run(conf) // Right(true)
checkSpec("Linux", 1).run(conf) // Left(2 GB memory needs in Linux)
checkSpec("Mac", 16).run(conf) // Left(Mac is not supported)

正直なところ Scala だと 引数をグループ化する、 implicit を使う、クラスを使うといった代替手段があるので Reader モナドを使うかは個人的には悩ましいですね。

Writer モナド

書き込み専用の State のようなものです。
計算の前後とかでログを取っておきたい場合に有効です。

Writer[L,V] の型パラメータを取ります。 L はログを累積するための型で Monoid である必要があります。一般的に追加コストが定数オーダーの Vector などを選択します。
V は 計算に使う任意の型です。

L の型を Vector に固定したエイリアスを作り Writer インスタンスを作ってみます。
インスタンスを作るには Writer.apply を使ったり Applicative の pure を使ったりします。

import cats.data.Writer
// Vector をログにする。
type Logged[A] = Writer[Vector[String], A]

// apply から
val fromApply:Logged[Int] = Writer(Vector[String](), 3)

// アプリカティブから
val a = 3.pure[Logged] // Writer(Vector[String](), 3])

ログを追記するには tell を使用します。
また、例のごとく計算の実行は run で行います。

val logged = a.tell(Vector("start"))
val logged2 = logged.tell(Vector("second"))

logged2.run // (Vector(start, second),3)

tell を使うと前の値に追加されていくのがわかります。

値やログを変更するための map 系メソッドもあります。

// vだけ変える
a.map(_ + 1) // Writer(Vector[String(), 4])

// L だけ変える
a.mapWritten(_ => Vector("init")) // Writer(Vector[String]("init"), 3])

// 両方変える
a.mapBoth{case (l, v) => (Vector("a"), v * v)} //  Writer(Vector[String]("init"), 9])

とはいえ、変更や追記のたびに値が生成されるのは State と同様に変数が増えて面倒なので、こういった処理は for 式でまとめます。
Writer の flatMap は前の Writer 値のログをすべて引き継いて追記するので、for 式中で生成したログの中身はすべて保存されます。

val f = for (
    a <- 10.pure[Logged];
    _ <- Vector("start").tell; // startログを生成
    b <- 3.pure[Logged].map(_ + a).tell(Vector("add 3"));
    _ <- Vector("end").tell // endログ
) yield (b)

f.run //(Vector(start, add 3, end),13)

Future で行う並列計算の戻り値を Writer にしておくと、
並列計算の中で起きた出来事を記録できます。
そうすると、並列計算単位でまとまったログが取れるので便利です。

Monad Transformer

いろんなモナドを使いだすと複数のモナドがネストするという状況が出てきます。

例えば DB に接続して何らかの存在確認の SQL を発行するという処理を考えてみます。
DB 接続時に何かエラーが発生した場合のエラーを保持するために Either を使用し、
SQL の結果確認のために Option を使用します。

すると次のようなに Either[Option] が生まれます。

case class User(name:String)
// 実装は仮のもの。
def lookupUserFromDb(id:String):Either[Error, Option[User]] = Right(Option(User("a")))

上記メソッドを呼び出して User にアクセスして加工する場合
次のように for 式をネストさせる必要があります。

val userName:Either[Error, Option[String]] = 
for (result <- lookupDb("hoge")) yield {
  // RightProjection より Rightの場合だけここにくる
  for (user <- result) yield  user.name // Someの場合だけここにくる
}

このようなネストしたモナドに簡単にアクセスするために Monad Transformer が使えます。

Monad Transformer は基本的に各 Monad インスタンスごとに作成する必要があります。
Cats では、 OptionT, ListT, ReaderT など大体のモナドには用意されています。
(Eff モナドを使うと Monad Transformer 無しでも似たようなことができますが今回は触れません)

各 Monad Transformer のインスタンスは F[G] を受けて F をはがし、元のモナドに委譲する関数が定義してあり、結構力技でやっている印象です。
自作の Monad インスタンスに対して Monad Transformer を作る場合はそれなりの手間がかかりそうです。

前述の Either[Option] を Monad Transformer にする場合内側に包まれるモナドから Monad Transformer のスタックを構築します。
型の表記上は OptionT[Eihter] のように、内から外の順番になります。

前述の lookupUserFromDb メソッドを OptionT で直してみます。

注意点として、Monad Transformer は型パラメータを1つしか扱えないので、 Either のような複数の型パラメータを取るものはエイリアスを使って型パラメータを1つに固定させる必要があります。
今までも何度かエイリアスを使っていましたが見やすさの他にもこのような制約もあってのことでした。

// Eitherの型パラメータを固定
type ErrorOr[A] = Either[Error, A]
// Monad Transformer もエイリアスにする。Either[Option] なら、 OptionT[Either] の順になり逆転する。
type ErrorOrOption[A] = OptionT[ErrorOr, A]

def lookupDbMT(id:String):ErrorOrOption[User] = id match {
    // DB接続OKで、Someを返す場合は、OptionT.some
    case "ok" => OptionT.some(User("ok"))
    // pureを使えば Monad Transformerの種類問わず同じ方法で作成できる。
    case "ok2" => User("ok2").pure[ErrorOrOption]
    // DB接続OKで、Noneを返す場合
    case "ng" => OptionT.none
    // option関係なく、前段の Eitherの値を返す場合、 liftF を使う
    case _ => OptionT.liftF( (Left(new Error("connection error"))):ErrorOr[User] )
}

外側のモナド値が OK の場合で内側のモナド値を生成するには、 OptionT のメソッド (some, none) や pure を使います。

外側のモナド値が NG で内側のモナドに到達できないケースでは、 OptionT の liftT を使って外側のモナド値を設定します。

こうして作った Monad Transformer 値は map などで直接内部の値にアクセスできます。

def getName(id:String):ErrorOrOption[String] = lookupDbMT(id).map((u:User) => u.name)

前述の for式をネストさせるのと比べればとても簡単になったと思います。

最終的に値を取り出すには value を使います。
value で、Monad Transformer から通常の Either[Option] のネストモナド値を得ることができるので
普通の方法で値を取得します。

// TODO:本来はパターンマッチなどを使う。
getName("ok").value.right.get.get   // ok
getName("ok2").value.right.get.get  // ok2
getName("ng").value.right.get       // None
getName("error").value.left.get     // Error("connection error")

余談ですが、Monad Transformer の多段ネストも可能です。その場合は value.value のように value も複数回呼びます。

Kleisli

Kleisli Arrow (クライスリ射) という圏論の概念を扱うものだと思います。多分。

Arrow (射) はプログラム的な観点で話せば 関数 A=>B のような型から型への写像を F[A, B] として型クラスの世界で表現し、関数合成などの便利メソッドを提供するものだと思います(多分)。

Kleisli は Arrow の中でもモナディック関数 ( A => F[B] ) の関数合成を扱うものです。
端的に言えば A => F[B]B => F[C] の関数を A => F[C] に合成する機能を提供します。

import cats.data.Kleisli

def ab = (i:Int) => i.some // A => F[B]
def bc = (i:Int) => ("str"+i).some // B => F[C]
def composed = Kleisli(ab) andThen Kleisli(bc) // A => F[C]

Kleisli の関数を実行するのはやはり run です。

composed.run(8) // Some(str8str8)

基本は上記の通りですが他にいくつか派生メソッドがあります。

// g.f  に相当する compose も可能。
val cf = Kleisli(bc) compose Kleisli(ab)

// mapで戻り値を変換する。
composed.map(i => i + i).run(8) // Some(str8str8)

別のモナド値への変換もできます。

// mapFだと、別のモナド値に変換できる。
composed.mapF(some => List(some.get)).run(8) // List(str8)

せっかく Arrow に触れたので高カインド型間の Arrow である FunctionK を使って、モナド値に変換する例も紹介します。

import cats.arrow._
val o2l: FunctionK[Option, List] = new FunctionK[Option, List] {
    def apply[A](l: Option[A]): List[A] = List(l.get)
}
// mapK は、上記の FunctionK を受け取って、 mapF と同じように動く。
composed.mapK(o2l).run(8) // List(str8)

// FunctionK は次のように ~> や、 kind-projector というプラグインで与えられるλを使って、少し短く定義できる。
// なお、λ を使うと、 IDEA上ではコンパイルエラー表示されるので注意。実行はできる。
import cats.~>
val o2lOhter:FunctionK[Option,List] = λ[FunctionK[Option, List]](a => List(a.get))
composed.mapK(o2lOhter).run(8) // List(str8)

Kleisli はどこで使うんでしょうね。こういうケースで使うべきというのが私には思いつきませんでした。

次の節で説明するように内部的にこっそり使われているというのが多いような気がします。

ただし、モナディック関数の合成とは言ったものの A => F[B] 関数の F については必ずしも Monad インスタンスである必要はありません。
Kleisli では関数に応じて、F に必要とされる型クラスが違ったりします。
例えば、 map では F は Functor インスタンスであれば十分です。
compose や andThen だと、 FlatMap インスタンスでなければなりませんが、 Monad である必要がありません。

FlatMap は flatMap ができる性質の型クラスです。 Monad は FlatMap と Applicative の2つを継承しています。(FlatMap だけど Monad じゃないという型はほとんど無いんじゃないかと思いますが)

そのため Kleisli はモナディック関数というよりは高カインド型を生成する関数の合成といった方が適切ですし、
モナドよりも弱い制約の値をうまく扱えるのかもしれません。

こんな使い道があるぞというのを知っている人がいたら教えてください。

ReaderT と Kleisli

Reader[A,B] は実は、 ReaderT[Id, A, B] のエイリアスだったりします。
さらに ReaderT は Kleisli[F,A,B] のエイリアスです。
Kleisli は A=> F[B] の関数を扱うものでしたので、 Reader で扱っていた A=>B という関数は実際は
A => Id[B] なのでした。

Id がモナドでもあり値でもあるという性質がうまく使われています。
ということは ReaderT を直接使えば任意のモナド値の関数でもって Reader を扱うことができます。

前述の Reader のサンプルコードでは for 式中に Some がいくつも出てきてちょっと残念な感じでしたが。
ReaderT[Option] にすることで、Monad Transformer として扱えます。

// Reader ではなく、 ReaderT で宣言。
type ConfigOptionReader[A] = ReaderT[Option, Config, A]

// ReaderTの値として、Option値を返す。
def getSupportedOS(name:String):ConfigOptionReader[String] = ReaderT{m => m.os.find(_ == name)}

def getMemorySizeForOS(os:String):ConfigOptionReader[Int] = ReaderT{m => m.memory.get(os)}

def checkSpec(os:String, memory:Int):ConfigOptionReader[Either[String, Boolean]] ={
    for ( _os <- getSupportedOS(os);
        size <- getMemorySizeForOS(_os)
    )yield {
        // configから値が取得できた場合だけここにくる。取得できない場合は Noneになる。
        if(size < memory) true.asRight[String]
        else s"${size} GB memory needs in ${os}".asLeft[Boolean]
    }
}

val conf = Config(List("Windows", "Linux"), Map("Windows" -> 3, "Linux" -> 2))
// 結果は、 Some(Either) になるため、動きが少々変わる。
checkSpec("Windows", 4).run(conf) // Some(Right(true))
checkSpec("Linux", 1).run(conf)   // Some(Left(2 GB memory needs in Linux))
checkSpec("Mac", 16).run(conf)    // None 

checkSpec 内で、Option を取り扱う必要がなくなりすっきりしたと思います。

ReaderT が Kleisli になるのはよくわからならいですが、 公式には Kleisli は、関数型モナドの Monad Transformer とみなせると書いてあり、やっぱりよくわかりませんでした。

もう少し勉強します。

Validated

次の2つの属性をもつ User クラスがあるとします。

case class User(name:String, age:Int)

画面などから name, age の値を入力してもらってバリデーションを行いどちらもOKならUserを生成し、
どちらか NG なら エラーメッセージを出すという例を実装してみます。

name, age のそれぞれの入力チェックを関数として表現しそれを合成するという方向性でいってみます。

まずは Either でエラーメッセージと User を表現してみます。
エラーメッセージは複数あり得るので Vector としてみます。

type ErrorOr[A] = Either[Vector[String], A]

// 単品チェック関数
def checkName(name:String):ErrorOr[String] =
    if (name.length > 5) Right(name)
    else Left(Vector("name length is greater than 5"))

def checkAge(age:Int):ErrorOr[Int] =
    if (age >= 1) Right(age)
    else Left(Vector("age is greater than 0"))

チェック関数を合成してバリデーションを構築します。

// Apply で合成。 どちらもOKなら mapN で チェックOKの値を使って、User 生成
def validateUser(name:String, age:Int):ErrorOr[User] =
    (checkName(name), checkAge(age)).mapN(User(_, _))

やってみます。

validateUser("aaaaaa", 1)  // Right(User(aaaaaa,1))
validateUser("aaaaaa", -1) // Left(Vector(age is grater than 0))
validateUser("aaaa", 1)    // Left(Vector(name length is grater than 5))
validateUser("aaaa", -1)   // Left(Vector(name length is grater than 5)) <-ファーストエラーで止まっている。

最後のバリデートは2項目どちらも NG の値を入れていますがエラーメッセージは最初のチェックでエラーになったものだけです。

Either は、 Apply であると同時に Monad です。その場合 mapN の呼び出しは flatMap に置き換えられるようになっています。

そのため validateUser は次の処理と同じです。

def validateUser2(name:String, age:Int):Errors[User] =
    for (n <- checkName(name); a <- checkAge(age))yield (User(n, a))

モナドは逐次計算であり Either や Option では、最初の計算で NG になると後続の計算は行われません。
そのため最初のチェックが NG なら後続のチェックは行われません。

これをどうにかするために Validated があります。
Validated は Either と似ていて Valid と Invalid の2つの状態を持ちます。
また Validated は Applicative インスタンスですが Monad インスタンスではありません。
これがエラー値の累積において重要な意味を持ちます。

Either を返していたチェック関数を Validated を返すように実装し直してみます。
Validate はコンストラクタを使うか Eihter から変換するかの2種類の方法で作成できます。

import cats.data.Validated
type ErrorsOr[A] = Validated[Vector[String], A]

// Validateコンストラクタを使う
def checkNameV(name:String):ErrorsOr[String] =
    if (name.length > 5) Validated.valid(name)
    else Validated.invalid(Vector("age is grater than 0"))

// Either から変換する
def checkAgeV(age:Int):ErrorsOr[Int] = checkAge(age).toValidated

入力文字列から User を生成するのですから年齢 age については数字以外の入力に対するチェックも考慮してあげる必要があります。
Validated は合成可能ですので数字チェックを行う関数を作って、それを上記の年齢チェック関数と合成します。

// 数字なら Int へ変換するチェック
def isNumber(input:String):ErrorsOr[Int] =
    if (input.matches("^-?[0-9]+$")) Validated.valid(input.toInt)
    else Validated.invalid(Vector(s"${input} is not a number"))

// 年齢チェック関数と合成
def checkAgeFull(ageStr:String):ErrorsOr[Int] = isNumber(ageStr) andThen (checkAgeV)

これで準備が整いました。チェック関数を合成しバリデートを実行してみます。

// Applyの mapN で合成する。Validatedは Monadでないため、 for式での合成はできない。
def validateUserV(name:String, ageStr:String):ErrorsOr[User] =
    (checkNameV(name), checkAgeFull(ageStr)).mapN(User)

validateUserV("aaaaaa", "1")  // Valid(User(aaaaaa,1))
validateUserV("aaaaaa", "-1") // Invalid(Vector(age is grater than 0))
validateUserV("aaaa", "1")    // Invalid(Vector(name length grater than 5))
validateUserV("aaaa", "num")  // Invalid(Vector(name length grater than 5, num is not a number))

Validated は Monad でないため for 式や flatMap での合成はできません。
そのため flatMap による逐次計算の性質がなくなり、独立した各値の計算結果が Apply と Monoid によりエラーメッセージの累積として現れました。
これが、Monad ではない Applicative を使ってできる例の一つです。

まとめ

3回にわたって 「Advanced Scala with Cats」 の内容を紹介してきました。
なるべく圏論や内部には踏み込まず、型クラスを使ってこんな便利なプログラムが書けるぞという観点で記述してきました。
FP は抽象度が高いため何のために役立つのかが分かりづらい部分があると思いますが、
少しでも便利さが伝われば幸いです。

紹介しきれませんでしたが PDF には型クラスによるユースケースといった内容もあるので、
気になった方は PDF も見てみてください。
また公式サイトも (英語ですが) 内容がとても充実しています。

そしてまだまだ勉強不足だと感じました。

PDF の内容をマスターしたとは思ってないですし、PDF で登場した以外の型クラスも Cats にはまだあります。
また、この内容を超えた先には Free、Lens、EFF といった高度なモナドや Cats を使ったライブラリ (fs2, doobie, circe とか)がありますし、
さらに高抽象度の shapeless、マクロの scala.meta といった高みがあります。

何か違う・こうすると便利だみたいな意見があればどしどしコメントください。

また弊社では少ないながら Scala の案件もありますし、Scala に興味のある仲間もいます。

Scala を書きたい方、 Scala を使ったお仕事のご依頼をいただける方、私に Scala を書かせたい方をお待ちしております。