4

I wrote this simple program in my attempt to learn how Cats Writer works

import cats.data.Writer
import cats.syntax.applicative._
import cats.syntax.writer._
import cats.instances.vector._

object WriterTest extends App {
   type Logged2[A] = Writer[Vector[String], A]
   Vector("started the program").tell
   val output1 = calculate1(10)
   val foo = new Foo()
   val output2 = foo.calculate2(20)
   val (log, sum) = (output1 + output2).pure[Logged2].run
   println(log)
   println(sum)

   def calculate1(x : Int) : Int = {
      Vector("came inside calculate1").tell
      val output = 10 + x
      Vector(s"Calculated value ${output}").tell
      output
   }
}

class Foo {
   def calculate2(x: Int) : Int = {
      Vector("came inside calculate 2").tell
      val output = 10 + x
      Vector(s"calculated ${output}").tell
      output
   }
}

The program works and the output is

> run-main WriterTest
[info] Compiling 1 Scala source to /Users/Cats/target/scala-2.11/classes...
[info] Running WriterTest 
Vector()
50
[success] Total time: 1 s, completed Jan 21, 2017 8:14:19 AM

But why is the vector empty? Shouldn't it contain all the strings on which I used the "tell" method?

Knows Not Much
  • 30,395
  • 60
  • 197
  • 373

2 Answers2

4

When you call tell on your Vectors, each time you create a Writer[Vector[String], Unit]. However, you never actually do anything with your Writers, you just discard them. Further, you call pure to create your final Writer, which simply creates a Writer with an empty Vector. You have to combine the writers together in a chain that carries your value and message around.

type Logged[A] = Writer[Vector[String], A]

val (log, sum) = (for {
  _ <- Vector("started the program").tell
  output1 <- calculate1(10)
  foo = new Foo()
  output2 <- foo.calculate2(20)
} yield output1 + output2).run

def calculate1(x: Int): Logged[Int] = for {
  _ <- Vector("came inside calculate1").tell
  output = 10 + x
  _ <- Vector(s"Calculated value ${output}").tell
} yield output

class Foo {
  def calculate2(x: Int): Logged[Int] = for {
    _ <- Vector("came inside calculate2").tell
    output = 10 + x
    _ <- Vector(s"calculated ${output}").tell
  } yield output
}

Note the use of for notation. The definition of calculate1 is really

def calculate1(x: Int): Logged[Int] = Vector("came inside calculate1").tell.flatMap { _ =>
  val output = 10 + x
  Vector(s"calculated ${output}").tell.map { _ => output }
}

flatMap is the monadic bind operation, which means it understands how to take two monadic values (in this case Writer) and join them together to get a new one. In this case, it makes a Writer containing the concatenation of the logs and the value of the one on the right.

Note how there are no side effects. There is no global state by which Writer can remember all your calls to tell. You instead make many Writers and join them together with flatMap to get one big one at the end.

HTNW
  • 27,182
  • 1
  • 32
  • 60
1

The problem with your example code is that you're not using the result of the tell method.

If you take a look at its signature, you'll see this:

final class WriterIdSyntax[A](val a: A) extends AnyVal {

   def tell: Writer[A, Unit] = Writer(a, ())

}

it is clear that tell returns a Writer[A, Unit] result which is immediately discarded because you didn't assign it to a value.

The proper way to use a Writer (and any monad in Scala) is through its flatMap method. It would look similar to this:

println(
  Vector("started the program").tell.flatMap { _ =>
    15.pure[Logged2].flatMap { i =>
      Writer(Vector("ended program"), i)
    }
  }
)

The code above, when executed will give you this:

WriterT((Vector(started the program, ended program),15))

As you can see, both messages and the int are stored in the result.

Now this is a bit ugly, and Scala actually provides a better way to do this: for-comprehensions. For-comprehension are a bit of syntactic sugar that allows us to write the same code in this way:

println(
  for {
    _ <- Vector("started the program").tell
    i <- 15.pure[Logged2]
    _ <- Vector("ended program").tell
  } yield i
)

Now going back to your example, what I would recommend is for you to change the return type of compute1 and compute2 to be Writer[Vector[String], Int] and then try to make your application compile using what I wrote above.

Denis Rosca
  • 3,409
  • 19
  • 38