Stateモナドの存在は公式ドキュメント等で知っていましたが、ステートフルなオブジェクトをステートレスな形で呼び出すためのオブジェクト、
という認識で個人的にあまり使い道が思いついていませんでした 😇
ところが最近偶然使い道に出くわして、便利だった体験をしたのでまとめてみたいと思います。
Stateモナドとは?
前述の通り、ステートフルなオブジェクトをステートレスな形で呼び出すためのオブジェクトです。
OOP真っ盛りの頃だったら何を言っているのか理解できなかったに違いないので、例を記してみました。
case class State[S, A](run: S => (A, S))
超簡略版ですが、基本的には「ステートS
を受け取り、結果A
と新しいステートS
を返す」だけです。
catsの公式ドキュメントなんかでは、乱数生成器がS
、生成される乱数がA
と言った具合で説明されていますね。
Cats公式ドキュメント
https://typelevel.org/cats/datatypes/state.html
便利だった体験
例: ユニークなファイル名を生成する処理
OSのファイルシステムにおいて、同じディレクトリに同じファイル名は存在できません。
そのためにファイルパスをユニーク化する処理を書いてみました。
import cats.data.State
import scala.annotation.tailrec
val PathPattern = "^(.*?)\\s*(?:\\((\\d+)\\))?(?:\\.([^.]+))?$".r
def unique(path: String): State[Set[String], String] = State { others =>
@tailrec
def loop(path: String): String = {
if (others.contains(path))
path match {
case PathPattern(init, null, null) => loop(s"$init (1)")
case PathPattern(init, null, ext) => loop(s"$init (1).$ext")
case PathPattern(init, count, null) => loop(s"$init (${count.toInt + 1})")
case PathPattern(init, count, ext) => loop(s"$init (${count.toInt + 1}).$ext")
}
else path
}
val newPath = loop(path)
others + newPath -> newPath
}
現在のコンテキストである「その他のファイルパス」をステート Set[String]
として定義し、新しいファイルパスString
を生成する具合です。
通常ならば、現在の他のファイルパスをどこかの変数に保存しておいて、ユニークなファイル名の生成が済むまでその変数を管理し続けなければなりません。
ところが、このStateモナドを利用した場合、その変数の管理を行わずに済んでしまいます。
val names = (for {
name1 <- unique("test.png")
name2 <- unique("test.png")
name3 <- unique("test (1).png")
} yield Seq(
name1,
name2,
name3
)).runA(Set()).value
println(names) // test.png, test (1).png, test (2).png
このようにステートを渡さずに唐突に関数を呼び出すだけで、結果を取得できてしまうのです。
スコープを定義するかのように最後に初期のステートSet[String]
を渡すだけなので、再利用性も高いですね。
immutableなので標準APIとの相性も良く、非同期処理中で呼び出しても安全なのも利点です。
まとめ
Stateモナドを用いて、一時的な変数の利用をコンテキストのスコープを宣言的に記述できる事ができました。
非常に抽象度が高く、様々な用途に適用できそうなので注意深く観察しながら開発を続けたいと思います 💪