12

Suppose I want to wrap code that can throw exceptions with a try-catch block that logs the exception and continues. Something like:

loggingExceptions {
  // something dangerous
}

Ideally, I would like to use for logging the Logger defined on the calling object, if any (and if none, get a compile-time error). I'd love to define something like this:

def loggingExceptions[L <: { def logger: Logger }](work: => Unit)(implicit objectWithLogger: L): Unit = {
  try {
    work
  } catch {
    case t: Exception => objectWithLogger.logger.error(t.getMessage)
  }
}

where objectWithLogger would somehow "magically" expand to "this" in client code. Is this (or a similar thing) possible?

Jean-Philippe Pellet
  • 59,296
  • 21
  • 173
  • 234

3 Answers3

15

It can in fact be done just as you want. The other answerers surrendered too quickly. No white flags!

package object foo {
  type HasLogger = { def logger: Logger }
  implicit def mkLog(x: HasLogger) = new {
    def loggingExceptions(body: => Unit): Unit =
      try body
      catch { case ex: Exception => println(ex) }
  }
}

package foo {
  case class Logger(name: String) { }

  // Doesn't compile:
  // class A {
  //   def f = this.loggingExceptions(println("hi"))
  // }
  // 1124.scala:14: error: value loggingExceptions is not a member of foo.A
  //         def f = this.loggingExceptions(println("hi"))
  //                      ^
  // one error found  

  // Does compile
  class B {
    def logger = Logger("B")
    def f = this.loggingExceptions(println("hi"))
    def g = this.loggingExceptions(throw new Exception)
  }
}

object Test {
  def main(args: Array[String]): Unit = {
    val b = new foo.B
    b.f
    b.g
  }
}

// output
//
// % scala Test
// hi
// java.lang.Exception
psp
  • 12,138
  • 1
  • 41
  • 51
  • 2
    +1 for the example. This _can_ be done, but it's inadvisable. It doesn't separate the logging concern as well as a Logging trait, requires "implicit magic", and incurs unnecessary compile-time (implicit search) and run-time (reflection) overhead. The implementation hides the fact that the implicit conversion needs to be in scope, and in a more realistic codebase this would require an import statement in the client code. – Aaron Novstrup Nov 24 '10 at 20:57
  • 6
    Your list of negatives sums up to "I'd rather solve a different problem." Well yeah, wouldn't we all. – psp Nov 24 '10 at 21:13
  • Not really. The fundamental problem is logging exceptions in a block of code within a class that has a logger. You can solve that problem the way the OP originally envisioned (with implicits), or you can solve it much more simply with a trait. – Aaron Novstrup Nov 24 '10 at 21:56
  • I just re-read the question. Maybe the logging use case was just a motivating example to get at a more general technique? *sheepish grin* – Aaron Novstrup Nov 24 '10 at 22:02
  • 1
    @Aaron The logging case was my actual problem to be solved, and I'm happy with the trait solution. But it's also useful to know how to "simulate" how such a "implicit-this substitute" could work. – Jean-Philippe Pellet Nov 24 '10 at 23:03
4

Debilski's answer will work, but I'm not sure I see a good reason to use a structural type (i.e. { def logger: Logger }) here. Doing so will incur extra runtime overhead whenever logger is invoked, since the implementation of structural types relies on reflection. The loggingExceptions method is closely tied to logging, so I would just make it part of a Logging trait:

trait Logging {
   def logger: Logger

   final def loggingExceptions(body: => Unit) =
      try body catch { case e: Exception => logger.error(e.getMessage) }
}

trait ConcreteLogging extends Logging { 
   val logger = // ...
}

object MyObject extends SomeClass with ConcreteLogging {
   def main {
      // ...
      loggingExceptions { 
         // ...
      }
   }
}
Community
  • 1
  • 1
Aaron Novstrup
  • 20,967
  • 7
  • 70
  • 108
3

You could add a trait to all classes which want to use def loggingExceptions and in this trait add a self-type which expects def logger: Logger being available.

trait LoggingExceptions {
    this: { def logger: Logger } =>
  def loggingExceptions(work: => Unit) {
    try { work }
    catch { case t: Exception => logger.error(t.getMessage) }
  }
}

object MyObjectWithLogging extends OtherClass with LoggingExceptions {
  def logger: Logger = // ...

  def main {
    // ...
    loggingExceptions { // ...
    }
  }
}
Debilski
  • 66,976
  • 12
  • 110
  • 133
  • Thanks, this works! But are there any other solutions that don't involve changing the declaration of all classes that wish to use loggingExceptions(...)? – Jean-Philippe Pellet Nov 24 '10 at 19:23
  • @JPP No, at the very least the callsite will need to have an implicit object of the expected type in scope. For example, you could make the implicit parameter a `Logger`, and change the `def logger` to `implicit def logger` in the calling object. However, implicits should be avoided unless necessary, and a trait is a good fit for this problem. – Aaron Novstrup Nov 24 '10 at 19:47