はじめに
関数型プログラミングでは、純粋関数を使ってプログラムを組み立てていくことで様々なメリットを享受できます。例えばFunctional Programming in Scalaには以下のように利点が列挙されています。
Because of their modularity, pure functions are easier to test, reuse, parallelize, generalize, and reason about.
しかし今日、ほとんどのアプリケーションでは、DBIOなどの副作用が必須です。そこで関数型プログラミングで副作用を扱うアプローチの一つとして、Freeモナドを紹介します。
モナドが怖いという人もいるかもしれませんが、あまり深く考えずに"Freeモナド"という関数型プログラミングのデザインパターンの一つとして考えると良いと思います。
Freeモナドを利用する
Freeモナドを利用することで、以下の2つを分離することができます。
- 処理の流れ
- 実際の処理方法
ざっくりした実装のイメージはこんな感じです。
// ここではまだ実行されていない
def 処理の流れ(userId: UserId, name: String) =
user <- "ユーザーを取ってくる(userId)"
_ <- "ユーザーを更新する(user.copy(name = name))"
_ <- "更新履歴を追加する(user.copy(name = name))"
処理方法 =
"ユーザーを取ってくる" なら 非同期で DBからユーザーを取得
"ユーザーを更新する" なら 非同期で UPDATEで名前を更新する
"更新履歴を追加する" なら 非同期で 履歴テーブルにINSERT
結果 = 処理する(処理の流れ(1001, "jooohn"), 処理方法)
ポイントは、副作用を起こす処理方法を切り出すことで、処理の流れを純粋な関数として記述できる点です。
また、処理の流れと別に切り出した処理方法を付け替えることもできます。
例えばtest用にインメモリDBを利用したり、ダミーデータを返したり、履歴をテーブルで持つ代わりにログとして吐いたり、というように、処理方法を変更することができます。DIやStrategyパターンに近いことができる、と考えると理解しやすいかもしれません。
実装例
Scala Catsを利用した実装例を紹介します。本家のドキュメントが素晴らしいので、抵抗が無い人はそっちを見ると良いと思います。
0. 切り出す命令をADTとして宣言
// この命令を実行したときに返却する型をAとする
sealed trait Operation[A]
case class FetchUser(userId: UserId) extends Operation[User]
case class UpdateUser(user: User) extends Operation[Unit]
case class SaveUserChangeHistory(user: User) extends Operation[Unit]
1. 宣言したADTをベースに、Free型を宣言する
何も考えずに宣言します。
import cats.free.Free
type MyFreeOperation[A] = Free[Operation, A]
2. Free型の値を簡単につくれるような関数郡を準備
各命令のFree型を簡単に作れるようなメソッドを宣言します。
import cats.free.Free.liftF
def fetchUser(userId: UserId): MyFreeOperation[User] =
liftF[Operation, User](FetchUser(userId))
def updateUser(user: User): MyFreeOperation[Unit] =
liftF[Operation, Unit](UpdateUser(user))
def saveUserChangeHistory(user: User): MyFreeOperation[Unit] =
liftF[Operation, Unit](SaveUserChangeHistory(user))
// このように命令を組み合わせることも簡単にできます。
// 今回はもともとがかなり具体的な命令になっていますが、もう少し上手く抽象化を行うと再利用しやすい命令セットが作れるでしょう。
def updateUserWithChangeHistory(user: User): MyFreeOperation[Unit] =
for {
_ <- updateUser(user)
_ <- saveUserChangeHistory(user)
} yield ()
3. 処理の流れを記述
def updateUserName(userId: UserId, name: String): MyFreeOperation[Unit] =
for {
user <- fetchUser(userId)
_ <- updateUserWithChangeHistory(user.copy(name = name))
} yield ()
val program = updateUserName(1001, "jooohn")
4. 処理方法を記述
ここで副作用を扱うような処理を書きます。
// ここのFutureは自分のユースケースにあったMonadインスタンスを選択
def compiler: Operation ~> Future =
new (Operation ~> Future) {
def apply[A](fa: Operation[A]): Future[A] =
fa match {
case FetchUser(userId) =>
// Future[User]を返す処理
case UpdateUser(user) =>
// updateする処理
case SaveUserChangeHistory(user) =>
// 更新履歴を保存する処理
}
}
ここではFutureを利用していますが、Monadのインスタンスであればなんでも良いです。FutureやEither、Optionなどを選択することで、ハッピーじゃないケースで"処理の流れ"を途中で失敗させる、Fail Fastな実装を実現できます。
本家の記事で例になっているId
は、失敗することがなく必ず素直に1つの値を返すので、テスト等に利用すると良いでしょう。
5. 実行する
val result: Future[Unit] = program.foldMap(compiler)
最後に
副作用を切り出すテクニックとしてFreeモナドを紹介しました。
Freeモナドは、再利用可能な純粋関数を使ってプログラムを構築し、副作用をまとめる、とても関数型的なテクニックです。書いている方はなんとなく悦に入れるので、関数型プログラミングをしている感を感じたい人にもオススメです。
なおボイラープレートが多くなりがちですが、Freestyleのようなライブラリもあります。