Search code examples
scalascala-cats

Ignored parameter in for-comprehension for cats.data.WriterT


I'm going through scala with cats. In the example of Writer ($4.7.2, p. 111) the following for-comprehension is used:

import cats.data.Writer
import cats.syntax.writer._
import cats.syntax.applicative._
import cats.instances.vector._
type Logged[A] = Writer[Vector[String], A]

val writer1 = for {
  a <- 10.pure[Logged]
  _ <- Vector("a", "b", "c").tell
  b <- 32.writer(Vector("x", "y", "z"))
} yield a + b
// writer1: cats.data.WriterT[cats.Id,Vector[String],Int] = WriterT((
Vector(a, b, c, x, y, z),42))

From what I know underscore (_) is used for ignored parameters, it is never used after yield keyword. Still values "a", "b", and "c" are written to the log. Is it an idiom or is there another explanation for that?


Solution

  • Writer monad can be thought of as a tuple where the first element represents log value whilst the second element represents the main business value. The key is to understand that the underscore _ in

    for {
      a <- 10.pure[Logged]
      _ <- Vector("a", "b", "c").tell
      b <- 32.writer(Vector("x", "y", "z"))
    } yield a + b
    

    stands for the "business value", that is, the second ._2 element of the tuple, and not the entire tuple, so only the business value is ignored at this point in the composition. Perhaps it would help if we de-sugared the for-comprehension

    WriterT[Id, Vector[String], Int](Vector(), 10).flatMap { (a: Int) =>
      WriterT[Id, Vector[String], Unit](Vector("a", "b", "c"), ()).flatMap { (_: Unit) =>
        WriterT[Id, Vector[String], Int](Vector("x", "y", "z"), 32).map { (b: Int) =>
          a + b
        }
      }
    }
    

    In this way we see nothing really unusual is happening; the argument (_: Unit) is simply not being used in the body of (_: Unit) => body. Now lets also have a look under the hood of flatMap

    def flatMap[U](
      f: V => WriterT[F, L, U]
    )(implicit flatMapF: FlatMap[F], semigroupL: Semigroup[L]): WriterT[F, L, U] =
      WriterT {
        flatMapF.flatMap(run) { lv =>
          flatMapF.map(f(lv._2).run) { lv2 =>
            (semigroupL.combine(lv._1, lv2._1), lv2._2)
          }
        }
      }
    

    Few things immediately pop

    • tuple notation ._1 and ._2
    • semigroupL.combine(lv._1, lv2._1)
    • f(lv._2)

    We see how semigroup is used to combine logs which are first elements of tuples. Analysing f(lv._2) in our case we have lv._2 = () and f is function (_: Unit) => body where the argument (_: Unit) is simply not being used in the body.

    In general the particular definition of flatMap is what gives monads their characteristic power. In the case of Writer this power allows it to transparently combine logs as we progress along the chain of computations.


    As a side note, as Luis says, there is no side-effect happening here. Consider the functional programming sense of the term effect in the following sentences

     - Identity monad encodes the effect of having no effect
     - IO       monad encodes the effect of having a side-effect
     - Option   monad encodes the effect of having optionality
    

    The semantics of effect are encoded by the implementation of flatMap.