как сохранить возвращаемое значение при входе в Scala - PullRequest
20 голосов
/ 12 марта 2012

При программировании на Java, я всегда записываю входной параметр и возвращаемое значение метода, но в scala последняя строка метода является возвращаемым значением. поэтому я должен сделать что-то вроде:

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

чтобы упростить, пишу утилиту:

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 _
}

Тогда я использовал это как:

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

оно запишет значение и вернет его. это работает для меня, но кажется странным. поскольку я старый программист на Java, но плохо знаком с Scala, я не знаю, есть ли более идиоматический способ сделать это в Scala.


спасибо за вашу помощь, теперь я создаю лучший утилит, используя комбинатор Kestrel, упомянутый 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)}
}

Я добавляю параметр f, чтобы передать ему логгер из slf4j, и контрольный пример:

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
  }
}

Ответы [ 6 ]

34 голосов
/ 12 марта 2012

То, что вы ищете, называется комбинатор Kestrel (K комбинатор): Kxy = x. Вы можете выполнять все виды операций с побочными эффектами (не только ведение журнала), возвращая переданное ему значение. Читать https://github.com/raganwald/homoiconic/blob/master/2008-10-29/kestrel.markdown#readme

В Scala самый простой способ реализовать это:

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

Затем вы можете определить свою функцию печати / регистрации как:

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

И используйте это как:

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

ваш пример функции становится однострочным:

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

Если вы предпочитаете OO-нотацию, вы можете использовать имплики, как показано в других ответах, но проблема такого подхода заключается в том, что вы будете создавать новый объект каждый раз, когда захотите что-то записать, что может привести к снижению производительности, если вы это сделаете достаточно часто. Но для полноты это выглядит так:

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

Используйте это как:

def myFunc() = calcSomeResult().log("result is")
7 голосов
/ 12 марта 2012

Если вам больше нравится более общий подход, вы можете определить

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
}

и используйте его как

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

calcSomeResult() tap println
6 голосов
/ 12 марта 2012

Вы правильно поняли основную идею - вам просто нужно немного привести в порядок ее, чтобы сделать ее максимально удобной.

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)

Теперь вы можете

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

Таким образомвам не нужно повторяться (например, нет rs дважды).

3 голосов
/ 12 марта 2012

Допустим, у вас уже есть базовый класс для всех логгеров:

abstract class Logger {
  def info(msg:String):Unit
}

Тогда вы можете расширить String с помощью метода ведения журнала @@:

object ExpressionLog {
  // default logger
  implicit val logger = new Logger { 
    def info(s:String) {println(s)}
  }

  // adding @@ method to all String objects
  implicit def stringToLog (msg: String) (implicit logger: Logger) = new {
    def @@ [T] (exp: T) = {
      logger.info(msg + " = " + exp)
      exp
    }
  }
}

Чтобы использовать ведение журнала, вам необходимо импортировать элементы объекта ExpressionLog, а затем вы можете легко записывать выражения, используя следующую запись:

import ExpressionLog._

def sum (a:Int, b:Int) = "sum result" @@ (a+b)
val c = sum("a" @@ 1, "b" @@2)

Напечатает:

a = 1
b = 2
sum result = 3

Это работает, потому что каждый раз, когда вы вызываете метод @@ на компиляторе String, вы понимаете, что String не имеет метода, и автоматически преобразует его в объект с анонимным типом, который имеет метод @@ определены (см. stringToLog). Как часть компилятора преобразования выбирает нужный регистратор как неявный параметр , таким образом вам не нужно постоянно передавать регистратор на @@ каждый раз, пока вы сохраняете полный контроль над тем, какие потребности регистратора использоваться каждый раз.

Что касается приоритета, когда метод @@ используется в инфиксной нотации, он имеет наивысший приоритет , что упрощает рассуждение о том, что будет регистрироваться.

Так что, если вы хотите использовать другой регистратор в одном из ваших методов? Это очень просто:

import ExpressionLog.{logger=>_,_}  // import everything but default logger
// define specific local logger 
// this can be as simple as: implicit val logger = new MyLogger
implicit val logger = new Logger { 
  var lineno = 1
  def info(s:String) {
    println("%03d".format(lineno) + ": " + s) 
    lineno+=1
  }
}

// start logging
def sum (a:Int, b:Int) = a+b
val c = "sum result" @@ sum("a" @@ 1, "b" @@2)

Будет выводить:

001: a = 1
002: b = 2
003: sum result = 3
1 голос
/ 04 сентября 2014

Компилируя все ответы, плюсы и минусы, я придумал это (контекст представляет собой приложение Play):

import play.api.LoggerLike

object LogUtils {

implicit class LogAny2[T](val value : T) extends AnyVal {

    def @@(str : String)(implicit logger : LoggerLike) : T = {
        logger.debug(str);
        value
    }

    def @@(f : T => String)(implicit logger : LoggerLike) : T = {
        logger.debug(f(value))
        value
    }
}

Как видите, LogAny - это AnyVal, поэтому не должно быть никаких накладных расходов на создание нового объекта.

Вы можете использовать его так:

scala> import utils.LogUtils._
scala> val a = 5
scala> val b = 7
scala> implicit val logger = play.api.Logger

scala> val c = a + b @@ { c => s"result of $a + $b = $c" }
c: Int = 12

Или, если вам не нужна ссылка на результат, просто используйте:

scala> val c = a + b @@ "Finished this very complex calculation"
c: Int = 12

Есть ли недостатки этой реализации?

Edit:

Я сделал это доступным с некоторыми улучшениями в гисте здесь

0 голосов
/ 10 февраля 2019

Начиная с Scala 2.13, операция связывания tap может использоваться для применения побочного эффекта (в данном случае некоторого ведения журнала) к любому значению при возврате исходного значения:

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

Например:

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

или в нашем случае:

import scala.util.chaining._

def myFunc() = calcSomeResult().tap(x => logger.info(s"result is: $x"))
...