简体   繁体   中英

How to Monadic Logging in Scala

I often want to log or to print something without changing it.

It looks like this:

val result = myResult // this could be an Option or a Future
      .map{r =>
        info(s"the result is $r")
        r
      }

These three lines are always the same.

In a for comprehension, this can be done a bit nicer.

But I look for a solution for the first declarative version. It should look like:

val result = myResult
      .log(info(s"the result is ${_}"))

This one-liner could be put in every place in the chain where there could be a map, like:

val result = myResult
      .log(info(s"1. ${_}"))
      .filter(_ > 1)
      .log(info(s"2. ${_}"))
      ...

How could this be achieved? If possible, without a functional library.

Ok so I decided to take a swing at this and I would like to retract my comment of

Maybe you can define a implicit class that has a method "log" acting on a Product?

I was confident that Future and all the monads (collections, options) shared a common ancestor, turns out I was wrong. I have the following solution without using Cats. This can be done in a much prettier way in cats, besides the aforementioned " flatTap ", and embelished with possibly cats.Ref or something.

Future is the obvious outliner here but as more exceptions come along you might need to expand this object.

import scala.concurrent._ 
import ExecutionContext.Implicits.global

object MonadicConv { 
  implicit class MonadicLog[+B <: Product, A] (val u: B){
    def log(l: String, args: List[A] = List()): B = {
      println("logging")
      println(u)
      println(l)
      u
    }  
  }



 implicit class FutureLog[T, A](val u: Future[T]){
    def log(l: String, args: List[A] = List()) : Future[T] = {
      println("logging")
      println(u)
      println(l)
      u
    }
  }
}

1) You will need to modify this with you own logging logic, I am just printing

2) I am not super proud of this as this is no longer a pure function. I am not sure if there is a work around this in Scala without using Cats. (There might be)

3) The args can be removed, just added them in case you want to pass in extra info

4) If you really want to combine these, you could try defining your own product, some leads: Implement product type in Scala with generic update function working on its parts

You can use this with

  import MonadicConv._


  val x = Some(5).log("").get
  val lx = List(Some(5), Some(10), Some(1)).log("list").flatten.log("x").filter(_ > 1).log("")
  val ff = Future.successful(List(Some(5), Some(10), Some(1))).log("fff").map(_.flatten.filter(_ > 1).log("inner")).log("")

This prints out

logging
Some(5)
option test
logging
List(Some(5), Some(10), Some(1))
list test
logging
List(5, 10, 1)
flat test
logging
List(5, 10)
filter test
logging
Future(Success(List(Some(5), Some(10), Some(1))))
future test
logging
Future(<not completed>)
incomplete test
logging
List(5, 10)
inner future test

Scastie version here

As I have mentioned, this is really the Cats land at this point. This is the best I could come up with in core Scala

For your purpose, it is best to use treelog. It turns the logging process and values into a Monad of DescribedComputation :

import treelog.LogTreeSyntaxWithoutAnnotations._
val result: DescribedComputation[YourValueType] = myResult ~> (_.fold("The result is empty")(r => s"The result is $r")

And usually to subtract the value from a DescribedComputation, use for comprehension:

for {
  res <- result
} {
  doSomethingTo(res)
}

See details from https://github.com/lancewalton/treelog


The whole example will look like:

val compRes = "Logging Result" ~< {
    for {
      r <- myResult ~> (_.fold("The result is empty")(r => s"The result is $r")
    } yield r
  }
}

for (res <- compRes) {
  doSomethingTo(res)
}

logger.info(logging.run.written.shows)

The output will look like:

2019-11-18 00:00:00,000 INFO Logging Result
  The result is XXX

Just for reference. ZIO provides this functionality nicely.

  /**
   * Returns an effect that effectfully "peeks" at the success of this effect.
   *
   * {{{
   * readFile("data.json").tap(putStrLn)
   * }}}
   */
  final def tap[R1 <: R, E1 >: E](f: A => ZIO[R1, E1, Any]): ZIO[R1, E1, A] = self.flatMap(new ZIO.TapFn(f))

There is even a version for the error case:

  /**
   * Returns an effect that effectfully "peeks" at the failure of this effect.
   * {{{
   * readFile("data.json").tapError(logError(_))
   * }}}
   */
  final def tapError[R1 <: R, E1 >: E](f: E => ZIO[R1, E1, Any]): ZIO[R1, E1, A]

This makes debugging really easy:

   myDangerousZioFunction
      .tapError(e => putStrLn(s"Server Exception: $e"))
      .tap(r => putStrLn(s"Result is $r"))
      ....

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM