2025-01-11

Reader Monad

Notes
This article was translated by GPT-5.2-Codex. The original is here.

This time I looked at the Reader monad and implemented the sample from the Reader monad page of All About Monads using Cats. I just started using Cats so there may be a better way to write it, but it works for now, so I’ll just post the code.

I think Cats MTL could make some parts more readable.

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
}

Compared to the Haskell code, the following points were different.

  • Haskell's asks corresponds to ReaderT.ask[Id, _].map(...) in Scala
    • There is no equivalent to asks (maybe?)
  • Haskell's mapM corresponds to traverse in Scala
  • Type inference doesn't work as well as in Haskell (maybe my style is not great) 😢

Since I rarely write this kind of code after starting work, I can’t help but think that procedural code might be clearer. So I plan to implement the same logic without the Reader monad and compare.