はじめに
今日も元気に副作用!あさだです!
今回、簡潔にまとまっているCatsの記事が少なかったので「Cats🐱に楽しく入門」をモチベに記事を作成しました。
また、手を動かして自分なりに解釈していく方が効率が良いと思うので実装ベースの説明になります。
注意
- Scala2での説明となります。
- 事前知識として暗黙の型変換(implicit -value, -parameter, -class)が必要となります。
目次
1. 記事について
- 当記事では Scala with Cats の内容を噛み砕いて説明します。
- 調査した内容を扱いますが解説に不備がある場合は当記事にコメント、もしくは @AerosmithBz までご連絡ください。
- 対象の読者としてScala/Javaプログラマを想定しております。
2. オススメする学習方法
以下おすすめの学習方法。
-
Scala with Cats を手を動かしながら読み進める。
Scala with Catsでは型クラスからモナドまでかなり優しく説明されています。
全文英語ですがDeepLなどを使用して読み進めるのが正直一番早いかと。 -
Haskell に入門する。
後述しますがCatsはHaskellに由来する型クラスを中心にAPIを提供しております。
Scala with Catsを熟読し実際に手を動かして学習するのであれば不要かと思いますが、
純粋関数型であるHaskellに一度入門してみるのも一つの手だと思います。
3. 型クラスの説明と実装
早速本題。ここが一番重要。
型クラスとは Haskell に由来するアドホック多相をサポートする型システムの機能です。
簡単にいうと、機能をカテゴライズして抽象化せん?ってやつ。(間違ってたらすまん)
説明文を読むより書きながら覚えた方が早いが以下の4項目は重要なので読んでください。
Scala with Catsでは型クラスを構成する重要な要素として以下の4項目を挙げており、
この4項目が最も重要で、押さえておくべきポイントとなる。(4項目めはオプション)
-
trait(type class)
型クラス。
抽象化された機能を定義する。 -
implicit value(type class instance)
型クラスのインスタンス。
具象化された機能を定義する。 -
implicit parameter(using type class)
型クラスの使用。
抽象化された型クラスを引数に取る。 -
implicit class(option utility)
ADTの値を使用したい時などにオプションで使用。
とりあえず実際に書いてみましょう😅
型クラスの実装
build.sbtに以下を追記する。
libraryDependencies += "org.typelevel" %% "cats-core" % "2.8.0"
Step1 - 4. implicit class を使用せずに実装してみる。
object Main extends App {
// とりまテキトーに代数的データ型を定義。
case class UserAccount(id: Long,
name: String,
password: String)
/**
* 1. trait: type class
* 型クラス Show を定義。
*/
trait Show[A] {
// ジェネリクスAを文字列にして返却する抽象メソッドを定義。
def show(value: A): String
}
/**
* 2. implicits value: type class instance
* 具象型クラスを定義。
*/
object ShowInstances {
// UserAccountに対応する具象メソッドを定義。
implicit val userAccountShow: Show[UserAccount] =
new Show[UserAccount] {
override def show(value: UserAccount): String =
s"UserAccount:id=${value.id},name=${value.name},password=${value.password}"
}
}
/**
* 3. implicit parameter: using type class
* 型クラスを使用するメソッドを定義。
*/
def printlnAsString[A](value: A)(implicit s: Show[A]) = println(s.show(value))
// 具象型クラスをスコープ内で使用出来るようにする。
import ShowInstances._
val userAccount = UserAccount(1L, "name", "password")
printlnAsString(userAccount) // UserAccount:id=1,name=name,password=password
}
暗黙の型変換を使用しているのが肝で、開発者は対応する型クラスをインポートするだけで済む。
Step2 - Javaで書いてみる。
上記のコードをJavaで書いてみると、なお理解が深まるかと。
public class Main {
public static void main(String[] args) {
var userAccount = new UserAccount(1L, "name", "password");
printlnAsString(userAccount, ShowInstances.show);
// UserAccount:id=1,name=name,password=password
}
// 代数的データ型
static class UserAccount {
public Long id;
public String name;
public String password;
public UserAccount(Long id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
}
/**
* 型クラス Show を定義。
*/
interface Show<A> {
// ジェネリクスAを文字列にして返却する抽象メソッドを定義。
String show(A value);
}
/**
* 具象型クラスを定義。
*/
static class ShowInstances {
private ShowInstances() {}
// UserAccountに対応する具象メソッドを定義。
static Show<UserAccount> show = new Show<UserAccount>() {
@Override
public String show(UserAccount value) {
return String.format(
"UserAccount:id=%d,name=%s,password=%s",
value.id,
value.name,
value.password);
}
};
}
/**
* 型クラスを使用するメソッドを定義。
*/
static <A>void printlnAsString(A value, Show<A> s) {
System.out.println(s.show(value));
}
}
暗黙の型変換が無い為(リフレクションを使用しない場合)具象型クラスのインスタンスが必要になるが、Scalaとそこまで大差ない。
Step3 - 4. implicit class を使用する。
object Main extends App {
case class UserAccount(id: Long,
name: String,
password: String)
trait Show[A] {
def show(value: A): String
}
object ShowInstances {
implicit val userAccountShow: Show[UserAccount] =
new Show[UserAccount] {
override def show(value: UserAccount): String =
s"UserAccount:id=${value.id},name=${value.name},password=${value.password}"
}
}
// --- ここまでは同じ
/**
* implicit class: interface syntax
* 暗黙の型変換を使用し拡張メソッドを定義する。
*/
object ShowSyntax {
implicit class ShowOps[A](value: A) {
def show(implicit s: Show[A]): String = s.show(value)
}
}
import ShowInstances._
import ShowSyntax._
val userAccount = UserAccount(1L, "name", "password")
println(userAccount.show) // UserAccount:id=1,name=name,password=password
}
impicit class ShowOps[A] により拡張メソッドとして型クラスのAPIを使用する事が出来る。
また、Catsでは impicit class ShowOps[A] のようなものを 構文(Syntax) と呼ぶ。
さて、書いていくうちに何となくのイメージは出来ただろうか。
ここまで理解出来てしまえばCatsのAPIもすんなりと使う事が出来ると思う。
よくわからんわって方は暗黙の型変換について再度学習してみると良いかも。
4. 猫に会う
ここからはCatsで遊んでみよう!!
とりあえずベーシックな型クラスとして Show、Eq、Monoid と遊んでみる。
ちな学習する際は以下を追記しておくと楽ちん。
import cats._
import cats.data._
import cats.implicits._
Show
先ほど作成した Showは既にCatsで定義されているのでまずはShowを使ってみよう。
trait Show[A] {
def show(value: A): String
}
object Main extends App {
import cats._
import cats.data._
import cats.implicits._
def putStrLn(value: String): Unit = println(value)
final val MAX_VALUE = 10000
putStrLn(MAX_VALUE.show) // 10000
// XXX.someを使用すると型がSome[A]ではなくOption[A]となる。
// CatsにはSome[A]のShowインスタンスが存在しない為、Option[A]にする必要がある。
val someHello: Option[String] = "HELLO".some
putStrLn(someHello.show) // Some(HELLO)
}
Eq
Eqは簡単に言うと論理演算子を提供する型クラス。
trait Eq[A] {
def eqv(a: A, b: A): Boolean
// other concrete methods based on eqv...
}
ここではimplicit value(型クラスインスタンス)を定義してみる。
object Main extends App {
import cats._
import cats.data._
import cats.implicits._
val hello_a = "hello"
val hello_b = "hello"
// Eqが提供する拡張メソッド`===`を使用。
println(hello_a === hello_b) // true
val some_a = "hello".some
val some_b = "hello".some
val none_a = none[String]
// OptionのEqを使用してみる。
println(some_a === some_b) // true
println(none_a === some_a) // false
case class Cats(name: String)
// CatsオブジェクトのEqインスタンスを定義。
implicit val catsEq: Eq[Cats] = new Eq[Cats] {
override def eqv(x: Cats, y: Cats): Boolean = x.name === y.name
}
val cats_a = Cats("foo")
val cats_b = Cats("boo")
// interface syntaxのおかげで拡張メソッドが使える。
println(cats_a === cats_b) // false
}
Monoid
Monoidは Semigroup という型クラスを継承しており、
combine と empty を提供する。
trait Monoid[A] {
def empty: A
def combine(x: A, y: A): A
}
- empty
ジェネリクスAにおける空の値を返却する。- empty: Int = 0
- empty: String = ""
- combine
ジェネリクスAのインスタンスを二つ受け取り、二つを足した値を返却する。- combine(10, 5): Int = 15
- combine("hello", "world"): String = "helloworld"
なお Semigroup は combine のみを提供。
object Main extends App {
import cats._
import cats.data._
import cats.implicits._
// combineの演算子として|+|を使用
println("hello" |+| "world") // "helloworld"
println(Monoid[String].empty) // ""
// 定番の`combineAll`を実装
// なおcombineAllはcats.Foldable.Opsにて既に定義されている。
def combineAll[A](l: Seq[A])(implicit m: Monoid[A]) =
l.foldLeft(m.empty)((acc,x) => acc |+| x)
println(combineAll(List(1,2,3))) // 6
println(combineAll(List("Hello", " ", "World"))) // Hello World
}
5. Functor
以降、Functor・Applicative・Monadと学習をしていくが、
挫折ポイントの名所な気がするのでなるべく優しく説明を行う。
とはいえFunctorも先ほど学んだShowやMonoidと同じ型クラスではある。
何が違うかというと高カインドをジェネリクスで使用する。
// 高カインドのジェネリクス例
def func[F[_], A](f: F[A]): Unit = println(f)
val x: Option[String] = Some("HELLO")
val y: List[Int] = List(1,2,3)
func(x) // Some(HELLO)
func(y) // List(1, 2, 3)
少し練習がてら先ほど作成したcombineAllの高カインドを抽象化してみる。
object Main extends App {
import cats._
import cats.data._
import cats.implicits._
// 抽象高カインドを使用する型クラスを定義
trait MyCombineAll[F[_]] {
def myCombineAll[A](a: F[A])(implicit m: Monoid[A]): A
}
// 型クラスのインスタンスを定義
implicit val listGet: MyCombineAll[List] = new MyCombineAll[List] {
override def myCombineAll[A](a: List[A])(implicit m: Monoid[A]): A =
a.foldLeft(m.empty)((acc,x) => acc |+| x)
}
// インターフェース構文を定義
implicit class MyCombineAllOps[F[_], A](a: F[A]) {
def myCombineAll(implicit c: MyCombineAll[F], m: Monoid[A]): A =
c.myCombineAll[A](a)
}
println(List(1,2,3).myCombineAll) // 6
}
本題の Functor が何かというと、お馴染みの map を提供する型クラス。
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
実際に Functorを使ってみる。
object Main extends App {
import cats._
import cats.data._
import cats.implicits._
// 理解を深める為 ジェネリックOptionを作成。
sealed trait Box[+A]
object Box {
def apply[A](value: A): Box[A] = Just(value)
def nothing[A]: Box[A] = Nothing
}
final case class Just[A](value: A) extends Box[A]
final case object Nothing extends Box[Nothing]
// インスタンスを作成する。
implicit def functorBox: Functor[Box] = new Functor[Box] {
override def map[A, B](fa: Box[A])(f: A => B): Box[B] = fa match {
case Just(value) => Just(f(value))
case Nothing => Nothing
}
}
// 注意点:Just[A]のインスタンスを作ってしまうとFunctorが機能しなくなってしまう。
val box_a = Box("hello").map(_ => 100)
val box_b = Box.nothing[String].map(_ => 100)
println(box_a) // Just(100)
println(box_b) // Nothing
// Functorを使ってみる。
def mapUsing[F[_], A](f: F[A])(implicit functor: Functor[F]) =
f.map(_ => 100)
println( mapUsing(Box("hello")) ) // Just(100)
}
便利〜〜〜〜!!!!
6. Applicative
続いて Applicative。
Applicative は Apply を継承した型クラスで、 Apply は Functor を継承している。
なお、Applyについては省略し、ここではHaskellのApplicativeに準じFunctorを継承したものとする。
trait Applicative[F[_]] extends Functor[F] {
def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]
def pure[A](a: A): F[A]
def map[A, B](fa: F[A])(f: A => B): F[B] = ap(pure(f))(fa)
}
- ap
F[A => B] を適用させる関数。 - pure
applyのようなもの。
object Main extends App {
import cats._
import cats.data._
import cats.implicits._
// 理解を深める為 ジェネリックOptionを作成。
sealed trait Box[+A]
object Box {
def apply[A](value: A): Box[A] = Just(value)
def nothing[A]: Box[A] = Nothing
}
final case class Just[A](value: A) extends Box[A]
final case object Nothing extends Box[Nothing]
implicit def applicativeBox: Applicative[Box] = new Applicative[Box] {
override def pure[A](x: A): Box[A] = Box(x)
override def ap[A, B](ff: Box[A => B])(fa: Box[A]): Box[B] =
(ff, fa) match {
case (x: Just[A => B], y: Just[A]) => Just(x.value(y.value))
case _ => Nothing
}
}
// ap の演算子<*>を使用
val box_a = Box((n: Int) => n.show) <*> Box(100)
val box_b = Applicative[Box].pure("HELLO")
val box_c = Box.nothing[Int].map(n => n.show)
println(box_a) // Just(100)
println(box_b) // Just(HELLO)
println(box_c) // Nothing
}
7. Monad
続いてモナド。
Monad は Applicative と FlatMap を継承した型クラス。
なお、FlatMap については省略し、HaskellのMonadに準じApplicativeを継承したものとする。
加えてCatsのMonadでは tailrecM という関数の定義が必要となる為独自で以下の型クラスを定義する。
tailrecMは便利な関数だがここで説明するとややこしくなる為割愛。
trait Monad[F[_]] extends Applicative[F] {
def flatMap[A, B](value: F[A])(func: A => F[B]): F[B]
}
- flatMap
A => F[B]を適用させる関数。
object Main extends App {
import cats._
import cats.data._
import cats.implicits._
// 理解を深める為 ジェネリックOptionを作成。
sealed trait Box[+A]
object Box {
def apply[A](value: A): Box[A] = Just(value)
def nothing[A]: Box[A] = Nothing
}
final case class Just[A](value: A) extends Box[A]
final case object Nothing extends Box[Nothing]
// type class
trait MyMonad[F[_]] extends Applicative[F] {
def flatMap[A, B](value: F[A])(func: A => F[B]): F[B]
}
// type class instance
implicit def monadBox: MyMonad[Box] = new MyMonad[Box] {
override def pure[A](value: A): Box[A] = Box(value)
override def flatMap[A, B](value: Box[A])(func: A => Box[B]): Box[B] = value match {
case Just(value) => func(value)
case Nothing => Nothing
}
override def ap[A, B](ff: Box[A => B])(fa: Box[A]): Box[B] = (ff, fa) match {
case (x: Just[A => B], y: Just[A]) => Just(x.value(y.value))
case _ => Nothing
}
}
// type class syntax
implicit class MyMonadOps[F[_], A](a: F[A]) {
def flatMap[B](func: A => F[B])(implicit m: MyMonad[F]): F[B] =
m.flatMap(a)(func)
}
val box_a = Box((n: Int) => n.show) <*> Box(100)
val box_b = Box(100).flatMap(n => Box(n.show))
val box_c = Box.nothing[Int].map(n => n.show)
println(box_a) // Just(100)
println(box_b) // Just(100)
println(box_c) // Nothing
}
駆け足気味だがモナドの説明は以上となる。
前述したが、これらはあくまでHaskellにおけるFunctor・Applicative・Monadとなる為CatsでMonad等を使用する際は実際の実装に考慮してコーディングを行おう!
また、Functor・Applicative・Monadについては各々Funtor則、Applicative則、Monad則という実装する上での規則が存在する為、自身でインスタンスを定義する際はそちらも考慮してコーディングを行いましょう。
8. まとめ
関数型のこの辺については情報量が多い為だいぶ割愛しての説明となりました。(圏論や概念的なお話は難しくかつ説明が長くなるのでだいぶ端折りました。)
記事については随時更新していこうかなーと思います。
ps. こないだ初めて渋谷のクラブに行きカルチャーショックを受けました。