让我们从你的方法和其他方法之间简单、表面的区别开始Reader
方法,即您不再需要坚持config
任何地方。假设您定义了以下模糊巧妙的类型同义词:
type Configured[A] = ConfigSource => A
现在,如果我需要一个ConfigSource
对于某些函数,假设一个函数得到n列表中的第一个客户端,我可以将该函数声明为“已配置”:
def nthClient(n: Int): Configured[Client] = {
config => config.clients(n)
}
所以我们本质上是在拉一个config
凭空而来,任何时候我们需要的时候!听起来像依赖注入,对吧?现在假设我们想要列表中第一、第二和第三个客户的年龄(假设它们存在):
def ages: Configured[(Int, Int, Int)] =
for {
a0 <- nthClient(0)
a1 <- nthClient(1)
a2 <- nthClient(2)
} yield (a0.age, a1.age, a2.age)
为此,当然,您需要一些适当的定义map
and flatMap
。我不会在这里讨论这个问题,只是简单地说 Scalaz(或Rúnar 精彩的 NEScala 演讲 http://www.youtube.com/watch?v=ZasXwtTRkio, or Tony's http://vimeo.com/20674558您已经看到过)为您提供了您需要的一切。
这里重要的一点是ConfigSource
依赖及其所谓的注入大多是隐藏的。我们在这里可以看到的唯一“提示”是ages
属于类型Configured[(Int, Int, Int)]
而不是简单地(Int, Int, Int)
。我们不需要明确引用config
任何地方。
作为旁白,这就是我几乎总是喜欢思考单子的方式:它们隐藏它们的效果所以它不会污染你的代码流,同时明确声明效果在类型签名中。换句话说,你不必重复太多:你说“嘿,这个函数处理effect X" 在函数的返回类型中,不要再搞乱它了。
在这个例子中,效果当然是从某个固定的环境中读取。您可能熟悉的另一个一元效应包括错误处理:我们可以说Option
隐藏错误处理逻辑,同时在方法的类型中明确出现错误的可能性。或者,与阅读相反,Writer
monad 隐藏了我们正在写入的内容,同时使其在类型系统中显式存在。
最后,就像我们通常需要引导 DI 框架一样(在我们通常的控制流之外的某个地方,例如在 XML 文件中),我们也需要引导这个奇怪的 monad。当然,我们的代码会有一些逻辑入口点,例如:
def run: Configured[Unit] = // ...
它最终变得非常简单:因为Configured[A]
只是函数的类型同义词ConfigSource => A
,我们可以将函数应用到它的“环境”:
run(ConfigFileSource)
// or
run(DatabaseSource)
哒哒!因此,与传统的 Java 风格 DI 方法相比,我们这里没有任何“魔法”。可以说,唯一的魔力被封装在我们的定义中Configured
类型及其作为 monad 的行为方式。最重要的是,类型系统让我们保持诚实关于哪个“领域”依赖注入发生在:任何具有类型的东西Configured[...]
存在于 DI 世界中,没有它的任何事物都不是。我们在老式 DI 中根本无法理解这一点,其中一切可能是由魔法管理的,因此您并不真正知道代码的哪些部分可以在 DI 框架之外安全地重用(例如,在单元测试中或完全在其他项目中)。
update:我写了一个博客文章 http://mergeconflict.com/reading-your-future这解释了Reader
更详细地说。