All You Need Is

Reader モナド

2025/01/11
ScalaCatsプログラミング関数型プログラミング

今回は Reader モナドに触れるということでモナドのすべての Reader モナドのページに載っているサンプルを Cats を使って実装してみた。 Cats を触り始めたばっかりなのでよりよい書き方があるかもしれないが、一旦動くところまで書けたのでコードだけ載せておく。

Cats MTL を使うともう少しわかりやすく書ける部分がありそうな気がしている。

1
import cats.Id
2
import cats.data.Kleisli.{ask, pure}
3
import cats.data.{Reader, ReaderT}
4
import cats.syntax.all.*
5
6
enum Template {
7
case Text(value: String)
8
case Variable(template: Template)
9
case Quote(template: Template)
10
case Include(template: Template, definitions: List[Definition])
11
case Compound(templates: List[Template])
12
}
13
14
case class Definition(name: Template, value: Template)
15
16
case class Environment(templates: List[(String, Template)], variables: List[(String, String)])
17
18
object Template {
19
20
def localVar(name: String, env: Environment): Option[String] =
21
env.variables.find(_._1 == name).map(_._2)
22
23
def lookupTemplate(name: String, env: Environment): Option[Template] =
24
env.templates.find(_._1 == name).map(_._2)
25
26
def addDefs(defs: List[(String, String)], env: Environment): Environment =
27
Environment(env.templates, defs ++ env.variables)
28
29
def resolveDef(definition: Definition): Reader[Environment, (String, String)] =
30
for {
31
name <- resolve(definition.name)
32
value <- resolve(definition.value)
33
} yield (name, value)
34
35
def resolve(template: Template): Reader[Environment, String] =
36
template match {
37
case Text(value) => pure(value)
38
case Variable(template) =>
39
for {
40
varName <- resolve(template)
41
varValue <- ask[Id, Environment].map(localVar(varName, _))
42
} yield varValue.getOrElse("")
43
case Quote(template) =>
44
for {
45
tmplName <- resolve(template)
46
body <- ask[Id, Environment].map(lookupTemplate(tmplName, _))
47
} yield body.map(_.toString).getOrElse("")
48
case Include(template, definitions) =>
49
for {
50
tmplName <- resolve(template)
51
body <- ask[Id, Environment].map(lookupTemplate(tmplName, _))
52
rendered <- body match {
53
case Some(bodyTmpl) => for {
54
defs <- definitions.traverse(resolveDef)
55
rendered <- ReaderT.local(addDefs(defs, _))(resolve(bodyTmpl))
56
} yield rendered
57
case None => ReaderT.pure[Id, Environment, String]("")
58
}
59
} yield rendered
60
case Compound(templates) =>
61
for {
62
resolved <- templates.traverse(resolve)
63
} yield resolved.mkString
64
}
65
}

Haskell のコードと比べると次の点が異なっていた。

働き始めてからこの手のコードを書く機会がまったく無くなってしまったので、手続き型でコードを書いた方がわかりやすいのはで?と思ってしまった。そのため、上記のコードと同じコードを Reader モナドを使わずに実装して比較してみようと思う。


Buy Me A Coffee