How to define a function that takes a function literal (with an implicit parameter) as an argument?
Asked Answered
H

2

3

I want to be able to do something on these lines (won't compile):

def logScope(logger:Logger)(operation: (implicit l:Logger) => Unit) {/* code */ operation(logger) /* code */} 
def operationOne(implicit logger:Logger) {/**/}
def operationTwo(implicit logger:Logger) {/**/}

And then use it like so:

logScope(new ConsoleLogger){logger =>
    operationOne
    operationTwo
    }

But the nearest I've come to a working solution is this:

def logScope(logger:Logger)(operation: Logger => Unit) {/* code */ operation(logger) /* code */} 
def operationOne(implicit logger:Logger) {/**/}
def operationTwo(implicit logger:Logger) {/**/}

/* other code */

logScope(new ConsoleLogger){logger =>
    implicit val l = logger
    operationOne
    operationTwo
    }

I don't think the language currently allows such constructs, but still, any suggestions or workarounds to achieve similar results?


minor update: I've created a gist with a slightly expanded version of the above code with a couple of attempts at simulating this kind of literal. As of now, CheatEx's version is the best one.

Happiness answered 15/8, 2011 at 18:33 Comment(8)
You might be interested in: #6628984Hinman
What's the motivation for this? There appears to be zero value to the block of code you've written. You explicitly define the implicit and then implicitly pass it to the code... why not just pass it explicitly in the first place? I get that you're trying to do a particular thing here - granted - but I think what you're doing is confusing as all hell... why not just pass the object in?Pasteurism
@derek It's the loan pattern at work. I agree the logScope code could be a little less dumber.Happiness
I get that it's the loan pattern - what I don't get is the hoops. Using operationOne(logger) and operationTwo(logger) would seem to be appropriate here.Pasteurism
@derek It becomes a bit tiresome if you have to use the scope in fifty different places. logScope could be sessionScope, fileScope or any number of other scopes with methods using a particular resource (whether injected, or otherwise). One need not have to specify the argument every time.Happiness
Yeah, I think I get it, but I'm pretty sure you can't loan an implicit like that. I've tried doing it a few different ways (probably the same ways you've tried) and it just doesn't scope.Pasteurism
@ben I looked at the example you linked to, and the accepted answer uses the same idea: assign the parameter to a local implicit variable in the function literal and then pass the block to the main function.Happiness
@derek I'm not too optimistic either. Anyway, this is a slightly longer version of the above example. As of now, the choice is between the local assignment, every time, and passing a local function as the argument.Happiness
J
7

In your second example try this:

logScope(Logger()) { implicit logger =>
  operationOne
}

It should work fine. The logic here is that 'implicit' is an attribute of particular value inside closure, not a part of the closure's interface.

Jovitta answered 15/8, 2011 at 20:36 Comment(2)
It does! The shortest way yet.Happiness
Can't get any better than this, I suppose.Happiness
C
2

Another solution is to rely on the dynamic scope pattern instead of implicit parameters. You can actually even combine both, like this:

import scala.util.DynamicVariable
object Logger {
  val defaultLogger = new ConsoleLogger( "DEFAULT: %s" )
  val currentLoggerVar = new DynamicVariable[Logger]( defaultLogger )
  implicit object DynamicScopeLogger extends Logger {
    def log( msg: Any* ) {
      currentLoggerVar.value.log( msg: _* )
    }
  }
}
trait Logger {
  def log( msg: Any* )
}
class ConsoleLogger( val pattern: String ) extends Logger {
  def log( msg: Any* ) { println( pattern.format( msg: _* ) ) }
}

def logScope[T](logger: Logger)( operation: => T ): T = {
  Logger.currentLoggerVar.withValue( logger )( operation )
}
def operationOne(implicit logger: Logger) { logger.log( "Inside operationOne" ) }
def operationTwo(implicit logger: Logger) { logger.log( "Inside operationTwo" ) }
def operationThree(implicit logger: Logger) { logger.log( "Inside operationThree" ) }
def operationFour(implicit logger: Logger) { logger.log( "Inside operationFour" ) }

A usage example:

operationOne
logScope(new ConsoleLogger("Customized Logger 1: %s")){
  operationTwo
  logScope(new ConsoleLogger("Customized Logger 2: %s")){
    operationThree
  }
  operationFour
}

Which results in:

DEFAULT: Inside operationOne
Customized Logger 1: Inside operationTwo
Customized Logger 2: Inside operationThree
Customized Logger 1: Inside operationFour

The current logger is passed implicitly "out of bounds" (we just use a global (and thread local) variable to store the current logger). We could every well never mention Logger anywhere in the method signatures, and directly call currentLoggerVar.value. Lifting the access to currentLoggerVar.value inside a default implicit Logger value (the DynamicScopeLogger proxy) allows us to keep the logging methods untouched. It also means that we can use dynamic scope by default, and override this behaviour when needed by simply defining a local Logger implicit that will then take precedence over DynamicScopeLogger.

The main disadvantages are:

  • Depending on the speed requirements, may be too slow: accessing thread local storage has a cost, including (but not limited to) a lookup in map of the thread local variables.

  • This relies on the fact that the lexical scoping matches the order of execution (which is generally the case, but not always). As soon as it's not the case anymore, you will run into troubles. By example when calling map or flatMap on a scala.concurrent.Future (or simply Future.apply), the body of the map/flatMap may be executed in another thread, and thus the body will not necessarily use the expected logger:

    scala>import scala.concurrent.Future
    import scala.concurrent.Future
    scala>import scala.concurrent.ExecutionContext.Implicits.global
    import scala.concurrent.ExecutionContext.Implicits.global
    scala>logScope(new ConsoleLogger("Customized Logger: %s")){
         |  Future{ operationOne }
         |}
    DEFAULT: Inside operationOne
    res5: scala.concurrent.Future[Unit] = scala.concurrent.impl.Promise$DefaultPromise@1a38913    
    

    In the above example, operationOne is called witihn the lexical scope of logScope, so we might expect to get the message "Customized Logger 1: Inside operationOne", however we see that the default logger is used instead. This is because the execution of the Future.apply's body is deferred and happens later on, on another thread (after we have reset the variable Logger.currentLoggerVar to its default value).

Commonwealth answered 6/11, 2012 at 14:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.