Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to keep return value when logging in scala

Tags:

logging

scala

When programming in java, I always log input parameter and return value of a method, but in scala, the last line of a method is the return value. so I have to do something like:

def myFunc() = {
  val rs = calcSomeResult()
  logger.info("result is:" + rs)
  rs
}

in order to make it easy, I write a utility:

class LogUtil(val f: (String) => Unit) {
 def logWithValue[T](msg: String, value: T): T = { f(msg); value }
}

object LogUtil {
  def withValue[T](f: String => Unit): ((String, T) => T) = new LogUtil(f).logWithValue _
}

Then I used it as:

val rs = calcSomeResult()
withValue(logger.info)("result is:" + rs, rs) 

it will log the value and return it. it works for me,but seems wierd. as I am a old java programmer, but new to scala, I don't know whether there is a more idiomatic way to do this in scala.


thanks for your help, now I create a better util using Kestrel combinator metioned by romusz

object LogUtil {
  def kestrel[A](x: A)(f: A => Unit): A = { f(x); x }
  def logV[A](f: String => Unit)(s: String, x: A) = kestrel(x) { y => f(s + ": " + y)}
}

I add f parameter so that I can pass it a logger from slf4j, and the test case is:

class LogUtilSpec extends FlatSpec with ShouldMatchers {
  val logger = LoggerFactory.getLogger(this.getClass())
  import LogUtil._

"LogUtil" should "print log info and keep the value, and the calc for value should only be called once" in {
  def calcValue = { println("calcValue"); 100 } // to confirm it's called only once 
  val v = logV(logger.info)("result is", calcValue)
  v should be === 100
  }
}
like image 545
诺 铁 Avatar asked Mar 12 '12 17:03

诺 铁


4 Answers

What you're looking for is called Kestrel combinator (K combinator): Kxy = x. You can do all kinds of side-effect operations (not only logging) while returning the value passed to it. Read https://github.com/raganwald/homoiconic/blob/master/2008-10-29/kestrel.markdown#readme

In Scala the simplest way to implement it is:

  def kestrel[A](x: A)(f: A => Unit): A = { f(x); x }

Then you can define your printing/logging function as:

def logging[A](x: A) = kestrel(x)(println)
def logging[A](s: String, x: A) = kestrel(x){ y => println(s + ": " + y) }

And use it like:

logging(1 + 2) + logging(3 + 4)

your example function becomes a one-liner:

def myFunc() = logging("result is", calcSomeResult())

If you prefer OO notation you can use implicits as shown in other answers, but the problem with such approach is that you'll create a new object every time you want to log something, which may cause performance degradation if you do it often enough. But for completeness, it looks like this:

implicit def anyToLogging[A](a: A) = new {
  def log = logging(a)
  def log(msg: String) = logging(msg, a)
}

Use it like:

def myFunc() = calcSomeResult().log("result is")
like image 72
romusz Avatar answered Nov 13 '22 05:11

romusz


If you like a more generic approach better, you could define

implicit def idToSideEffect[A](a: A) = new {
  def withSideEffect(fun: A => Unit): A = { fun(a); a }
  def |!>(fun: A => Unit): A = withSideEffect(fun) // forward pipe-like
  def tap(fun: A => Unit): A = withSideEffect(fun) // public demand & ruby standard
}

and use it like

calcSomeResult() |!> { rs => logger.info("result is:" + rs) }

calcSomeResult() tap println
like image 33
Debilski Avatar answered Nov 13 '22 05:11

Debilski


You have the basic idea right--you just need to tidy it up a little bit to make it maximally convenient.

class GenericLogger[A](a: A) {
  def log(logger: String => Unit)(str: A => String): A = { logger(str(a)); a }
}
implicit def anything_can_log[A](a: A) = new GenericLogger(a)

Now you can

scala> (47+92).log(println)("The answer is " + _)
The answer is 139
res0: Int = 139

This way you don't need to repeat yourself (e.g. no rs twice).

like image 6
Rex Kerr Avatar answered Nov 13 '22 04:11

Rex Kerr


Starting Scala 2.13, the chaining operation tap can be used to apply a side effect (in this case some logging) on any value while returning the original value:

def tap[U](f: (A) => U): A

For instance:

scala> val a = 42.tap(println)
42
a: Int = 42

or in our case:

import scala.util.chaining._

def myFunc() = calcSomeResult().tap(x => logger.info(s"result is: $x"))
like image 5
Xavier Guihot Avatar answered Nov 13 '22 05:11

Xavier Guihot