cómo mantener el valor de retorno al iniciar sesión en Scala
logging (5)
Al programar en java, siempre registro el parámetro de entrada y el valor de retorno de un método, pero en Scala, la última línea de un método es el valor de retorno Así que tengo que hacer algo como:
def myFunc() = {
val rs = calcSomeResult()
logger.info("result is:" + rs)
rs
}
Para hacerlo más fácil, escribo una utilidad:
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 _
}
Luego lo usé como:
val rs = calcSomeResult()
withValue(logger.info)("result is:" + rs, rs)
registrará el valor y lo devolverá. Funciona para mí, pero parece extraño. Como soy un viejo programador de Java, pero nuevo en Scala, no sé si hay una forma más idiomática de hacerlo en Scala.
gracias por su ayuda, ahora creo una mejor utilidad usando el combinador Kestrel mencionado por 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)}
}
Agrego el parámetro f para poder pasarle un registrador desde slf4j, y el caso de prueba es:
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
}
}
Compilando todas las respuestas, pros y contras, se me ocurrió esto (el contexto es una aplicación 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
}
}
Como puede ver, LogAny es un AnyVal, por lo que no debería haber ninguna sobrecarga en la creación de nuevos objetos.
Puedes usarlo así:
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
O si no necesita una referencia al resultado, simplemente use:
scala> val c = a + b @@ "Finished this very complex calculation"
c: Int = 12
¿Alguna desventaja de esta implementación?
Editar:
He hecho esto disponible con algunas mejoras en una esencia aquí
Digamos que ya tienes una clase base para todos tus registradores:
abstract class Logger {
def info(msg:String):Unit
}
Entonces podrías extender String con el método @@
logging:
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
}
}
}
Para usar el registro, tendría que importar miembros del objeto ExpressionLog
y luego podría registrar expresiones fácilmente usando la siguiente notación:
import ExpressionLog._
def sum (a:Int, b:Int) = "sum result" @@ (a+b)
val c = sum("a" @@ 1, "b" @@2)
Se imprimirá:
a = 1 b = 2 sum result = 3
Esto funciona porque cada vez que llama a un método @@
en un compilador String
da cuenta de que String
no tiene el método y lo convierte silenciosamente en un objeto con un tipo anónimo que tiene el método @@
definido (consulte stringToLog
). Como parte del compilador de conversión elige el registrador deseado como un parámetro implícito , de esta manera no tiene que seguir pasando el registrador a @@
cada vez que mantiene el control total sobre qué registrador debe usarse cada vez.
En cuanto a la prioridad, cuando se usa el método @@
en la notación de infijo, tiene la prioridad más alta, lo que hace que sea más fácil razonar sobre lo que se registrará.
Entonces, ¿qué sucede si desea utilizar un registrador diferente en uno de sus métodos? Esto es muy simple:
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)
Saldrá:
001: a = 1 002: b = 2 003: sum result = 3
Lo que estás buscando se llama Kestrel combinator (K combinator): Kxy = x
. Puede realizar todo tipo de operaciones de efectos secundarios (no solo el registro) mientras le devuelve el valor pasado. Lea https://github.com/raganwald/homoiconic/blob/master/2008-10-29/kestrel.markdown#readme
En Scala la forma más sencilla de implementarlo es:
def kestrel[A](x: A)(f: A => Unit): A = { f(x); x }
Luego puede definir su función de impresión / registro como:
def logging[A](x: A) = kestrel(x)(println)
def logging[A](s: String, x: A) = kestrel(x){ y => println(s + ": " + y) }
Y utilízalo como:
logging(1 + 2) + logging(3 + 4)
su función de ejemplo se convierte en una sola línea:
def myFunc() = logging("result is", calcSomeResult())
Si prefiere la notación OO, puede usar implícitos como se muestra en otras respuestas, pero el problema con este enfoque es que creará un nuevo objeto cada vez que quiera registrar algo, lo que puede causar una degradación del rendimiento si lo hace con la frecuencia suficiente. Pero para completar, se ve así:
implicit def anyToLogging[A](a: A) = new {
def log = logging(a)
def log(msg: String) = logging(msg, a)
}
Utilízalo como:
def myFunc() = calcSomeResult().log("result is")
Si te gusta un enfoque más genérico mejor, podrías definir
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
}
y usalo como
calcSomeResult() |!> { rs => logger.info("result is:" + rs) }
calcSomeResult() tap println
Usted tiene la idea básica correcta: solo necesita ordenarla un poco para que sea lo más conveniente posible.
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)
Ahora usted puede
scala> (47+92).log(println)("The answer is " + _)
The answer is 139
res0: Int = 139
De esta manera, no es necesario que se repita (por ejemplo, no dos veces).