21

请原谅这个问题的长度。

我经常需要在代码的一层创建一些上下文信息,并在其他地方使用这些信息。我通常发现自己使用隐式参数:

def foo(params)(implicit cx: MyContextType) = ...

implicit val context = makeContext()
foo(params)

这可行,但需要大量传递隐式参数,在干预函数布局后污染层的方法签名,即使他们自己不关心它。

def foo(params)(implicit cx: MyContextType) = ... bar() ...
def bar(params)(implicit cx: MyContextType) = ... qux() ...
def qux(params)(implicit cx: MyContextType) = ... ged() ...
def ged(params)(implicit cx: MyContextType) = ... mog() ...
def mog(params)(implicit cx: MyContextType) = cx.doStuff(params)

implicit val context = makeContext()
foo(params)

我觉得这种方法很难看,但它确实有一个优点:它是类型安全的。我确定mog会收到正确类型的上下文对象,否则它不会编译。

如果我可以使用某种形式的“依赖注入”来定位相关上下文,它将减轻混乱。引号表明这与 Scala 中常见的依赖注入模式不同。

起点foo和终点mog可能存在于系统的不同层次。例如,foo可能是用户登录控制器,并且mog可能正在执行 SQL 访问。可能有很多用户同时登录,但 SQL 层只有一个实例。每次mog由不同的用户调用,都需要不同的上下文。所以上下文不能被烘焙到接收对象中,你也不想以任何方式合并两个层(如蛋糕模式)。我也不想依赖像 Guice 或 Spring 这样的 DI/IoC 库。我发现它们很重,不太适合 Scala。

所以我认为我需要的是可以mog在运行时为它检索正确的上下文对象的东西,有点像ThreadLocal里面有一个堆栈:

def foo(params) = ...bar()...
def bar(params) = ...qux()...
def qux(params) = ...ged()...
def ged(params) = ...mog()...
def mog(params) = { val cx = retrieveContext(); cx.doStuff(params) }

val context = makeContext()
usingContext(context) { foo(params) }

但是一旦异步参与者参与到链中的任何地方,这种情况就会下降。你使用哪个actor库无关紧要,如果代码在不同的线程上运行,那么它会丢失ThreadLocal.

那么......我错过了一个技巧吗?一种在 Scala 中上下文传递信息的方法,它不会污染干预方法签名,不会静态地将上下文烘焙到接收器中,并且仍然是类型安全的?

4

5 回答 5

11

Scala 标准库包含类似于您假设的“usingContext”的东西,称为 DynamicVariable。这个问题有一些关于它的信息什么时候我们应该使用scala.util.DynamicVariable?. DynamicVariable 确实在后台使用了 ThreadLocal,因此您在 ThreadLocal 方面的许多问题仍然存在。

reader monad 是显式传递环境http://debasishg.blogspot.com/2010/12/case-study-of-cleaner-composition-of.html的功能替代方案。Reader monad 可以在 Scalaz http://code.google.com/p/scalaz/中找到。但是,ReaderMonad 确实“污染”了您的签名,因为它们的类型必须更改,并且通常一元编程可能会导致对您的代码进行大量重组,如果性能或内存是一个问题,所有闭包的额外对象分配可能不会很好。

这些技术都不会自动共享参与者消息发送的上下文。

于 2011-12-08T18:06:46.133 回答
7

聚会有点晚了,但是您是否考虑过在类构造函数中使用隐式参数?

class Foo(implicit biz:Biz) {
   def f() = biz.doStuff
}
class Biz {
   def doStuff = println("do stuff called")
}

如果您想为每次调用创建一个新 biz,f()可以让隐式参数成为返回新 biz 的函数:

class Foo(implicit biz:() => Biz) {
   def f() = biz().doStuff
}

现在您只需要在构建时提供上下文Foo。你可以这样做:

trait Context {
    private implicit def biz = () => new Biz
    implicit def foo = new Foo // The implicit parameter biz will be resolved to the biz method above
}

class UI extends Context {
    def render = foo.f()
}

请注意,隐式biz方法在UI. 所以我们基本上隐藏了这些细节:)

我写了一篇关于使用隐式参数进行依赖注入的博客文章,可以在这里找到(无耻的自我提升;))

于 2012-05-08T06:26:58.673 回答
2

我认为来自 lift 的依赖注入可以满足您的需求。有关使用 doWith () 方法的详细信息,请参阅wiki

请注意,即使您没有运行 lift,您也可以将其用作单独的库。

于 2011-12-08T17:23:56.490 回答
1

大约一年前你问过这个问题,但这是另一种可能性。如果您只需要调用一种方法:

def fooWithContext(cx: MyContextType)(params){
    def bar(params) = ... qux() ...
    def qux(params) = ... ged() ...
    def ged(params) = ... mog() ...
    def mog(params) = cx.doStuff(params)
    ... bar() ...
}

fooWithContext(makeContext())(params)

如果您需要所有方法在外部可见:

case class Contextual(cx: MyContextType){
    def foo(params) = ... bar() ...
    def bar(params) = ... qux() ...
    def qux(params) = ... ged() ...
    def ged(params) = ... mog() ...
    def mog(params) = cx.doStuff(params)
}

Contextual(makeContext()).foo(params)

这基本上是蛋糕模式,除了如果你所有的东西都放在一个文件中,你不需要所有杂乱的trait东西将它组合成一个对象:你可以嵌套它们。这样做也可以使cx词法范围正确,因此当您使用期货和演员等时,您最终不会出现有趣的行为。我怀疑如果您使用新的 AnyVal,您甚至可以消除分配Contextual对象的开销。

如果你想使用traits 将你的东西拆分成多个文件,你真的只需要一个trait文件来保存所有内容并将MyContextType正确的放在范围内,如果你不需要花哨的可替换组件通过继承的东西大多数蛋糕模式例子有。

// file1.scala
case class Contextual(cx: MyContextType) with Trait1 with Trait2{
    def foo(params) = ... bar() ...
    def bar(params) = ... qux() ...
}

// file2.scala
trait Trait1{ self: Contextual =>
    def qux(params) = ... ged() ...
    def ged(params) = ... mog() ...
}

// file3.scala
trait Trait2{ self: Contextual =>
    def mog(params) = cx.doStuff(params)
}

// file4.scala
Contextual(makeContext()).foo(params)

在一个小例子中看起来有点乱,但请记住,如果代码变得太大而无法舒适地放在一个文件中,您只需将其拆分为一个新特征。到那时,您的文件已经相当大了,因此在 200-500 行文件上额外添加 2 行样板文件确实不是那么糟糕。

编辑:

这也适用于异步的东西

case class Contextual(cx: MyContextType){
    def foo(params) = ... bar() ...
    def bar(params) = ... qux() ...
    def qux(params) = ... ged() ...
    def ged(params) = ... mog() ...
    def mog(params) = Future{ cx.doStuff(params) }
    def mog2(params) = (0 to 100).par.map(x => x * cx.getSomeValue )
    def mog3(params) = Props(new MyActor(cx.getSomeValue))
}

Contextual(makeContext()).foo(params)

只是使用嵌套工作。如果你能获得类似的功能,我会印象深刻DynamicVariable

您需要一个特殊的子类,它在创建时Future存储当前DynamicVariable.value,并挂钩到ExecutionContext'sprepare()execute()方法以提取并在执行之前value正确设置.DynamicVariableFuture

然后你需要一个特殊scala.collection.parallel.TaskSupport的东西来做类似的事情才能让并行集合工作。和一个特殊akka.actor.Props的为了做类似的事情

每次出现创建异步任务的新机制时,DynamicVariable基于实现的实现都会中断,并且您会遇到奇怪的错误,最终导致错误的Context. 每次添加新DynamicVariable的进行跟踪时,您都需要修补所有特殊执行程序以正确设置/取消设置这个新的DynamicVariable. 使用嵌套,您可以让词法闭包为您处理所有这些。

(我认为Futurescollections.parallelProps 算作“不是我的代码的层”)

于 2013-01-04T06:24:07.347 回答
1

与隐式方法类似,使用 Scala 宏,您可以使用构造函数自动连接对象 - 请参阅我的MacWire项目(请原谅自我推销)。

MacWire 也有作用域(非常可定制,ThreadLocal提供了一个实现)。但是,我认为您不能使用库在参与者调用之间传播上下文 - 您需要携带一些标识符。这可以例如通过用于发送参与者消息的包装器,或者更直接地使用消息。

然后,只要每个请求/会话/无论您的范围是什么,标识符都是唯一的,只需通过代理在地图中查找内容(就像 MacWire 范围一样,这里的“标识符”不需要,因为它存储在ThreadLocal) 中。

于 2013-04-30T13:30:22.583 回答