Debuggable: Using List Instead Of String (Scala 3 Video)
Motivation for this lesson
In this lesson I’ll (quickly) show a slight variation of the Debuggable
class. The motivations for this are:
- It’s important to get comfortable writing “wrapper” classes like this
- Many of the lessons that follow show more wrapper classes, and a simple variation now will help get your ready for those later
Source code
The source code for this lesson is at this URL:
From String
to List[String]
The change in this lesson is relatively simple: I’ll show how to implement Debuggable
with a List
to store the log messages, rather than using a String
as I did in the previous lessons.
Since I want to store the messages as a List[String]
, the first change is to the Debuggable
class signature, which now looks like this:
case class Debuggable[A](value: A, log: List[String]) {
------------
Inside the Debuggable
class, this change has no effect on map
. The log
parameter used to be a String
, and now it’s a List[String]
, and the code requires no changes:
def map[B](f: A => B): Debuggable[B] = {
val nextValue = f(value)
Debuggable(nextValue, this.log)
}
The change does affect flatMap
, which now prepends the old log
message before nextValue.log
when creating a new Debuggable
instance:
def flatMap[B](f: A => Debuggable[B]): Debuggable[B] = {
val nextValue: Debuggable[B] = f(value)
Debuggable(nextValue.value, this.log ::: nextValue.log)
}
The change also affects f
(and g
and h
), which now give Debuggable
a List
(rather than a String
):
def f(a: Int): Debuggable[Int] = {
val result = a * 2
Debuggable(result, List(s"f: multiply $a * 2 = $result"))
}
It also affects the final printing of the log messages:
// print a `List` (rather than a `String`)
finalResult.log.foreach(l => println(s"LOG: $l"))
The complete source code
With those changes, here’s the complete source code for the new version of Debuggable
:
// `log` is now a list
case class Debuggable[A](value: A, log: List[String]) {
def map[B](f: A => B): Debuggable[B] = {
val nextValue = f(value)
Debuggable(nextValue, this.log)
}
// prepend `this.log` before `nextValue.log`
def flatMap[B](f: A => Debuggable[B]): Debuggable[B] = {
val nextValue: Debuggable[B] = f(value)
Debuggable(nextValue.value, this.log ::: nextValue.log)
}
}
object DebuggableList extends App {
def f(a: Int): Debuggable[Int] = {
val result = a * 2
Debuggable(result, List(s"f: multiply $a * 2 = $result"))
}
def g(a: Int): Debuggable[Int] = {
val result = a * 3
Debuggable(result, List(s"g: multiply $a * 3 = $result"))
}
def h(a: Int): Debuggable[Int] = {
val result = a * 4
Debuggable(result, List(s"h: multiply $a * 4 = $result"))
}
val finalResult = for {
fRes <- f(100)
gRes <- g(fRes)
hRes <- h(gRes)
} yield s"result: $hRes"
finalResult.log.foreach(l => println(s"LOG: $l"))
println(s"Output is ${finalResult.value}")
}
The output of the App
looks like this:
LOG: f: multiply 100 * 2 = 200
LOG: g: multiply 200 * 3 = 600
LOG: h: multiply 600 * 4 = 2400
Output is result: 2400
Discussion
While this was a relatively minor change to the Debuggable
class, it’s helpful to see how that change has a ripple effect throughout the code. I showed this change so you could see that effect, but I also prefer this List
approach, which is a more “real world” example than the previous String
-based example.
The Writer monad
If you’re interested in where the Debuggable
class comes from, it’s actually an implementation of something known as the Writer monad in Haskell.
As Learn You a Haskell for Great Good! states, “the Writer monad is for values that have another value attached that acts as a sort of log value. Writer allows us to do computations while making sure that all the log values are combined into one log value that then gets attached to the result.”
Key points
In the last few lessons I showed several different versions of the Debuggable
class to help you get comfortable with it. Here are a few important points about it:
- A monad consists of a class with
map
andflatMap
methods, along with some form of a “lift” function. - A class built like this is intended to be used in
for
expressions. - I refer to these as “wrapper” classes. While they’re intended to be used in
for
expressions, they’re not “containers” like Scala collections classes, they’re more a type of “wrapper” or “box.” They wrap existing types to enable them to be used infor
expressions.
See also
When I was struggling to learn what a monad was, the following links were the best resources I found:
- An old Haskell tutorial, You Could Have Invented Monads
- Martin Snyder’s presentation, Monadic Logging and You
- Darren Wilkinson’s article, First steps with monads in Scala
You can read more about the Writer monad at these resources:
- The book, Learn You a Haskell for Great Good!
- The Writer monad section of the haskell.org wiki
Update: All of my new videos are now on
LearnScala.dev