Scala 中的配置数据 - 我应该使用 Reader monad 吗?


如何在

Scala 中创建功能正常的可配置对象?我已经看过托尼·莫里斯(Tony Morris)关于Reader monad的视频,但我仍然无法将这些点联系起来。

我有一个Client对象的硬编码列表:

class Client(name : String, age : Int){ /* etc */}
object Client{
  //Horrible!
  val clients  = List(Client("Bob", 20), Client("Cindy", 30))
}

我希望Client.clients在运行时确定,并且可以灵活地从属性文件或数据库中读取它。在 Java 世界中,我会定义一个接口,实现两种类型的源代码,并使用 DI 分配一个类变量:

trait ConfigSource { 
  def clients : List[Client]
}
object ConfigFileSource extends ConfigSource {
  override def clients = buildClientsFromProperties(Properties("clients.properties"))  
  //...etc, read properties files 
}
object DatabaseSource extends ConfigSource { /* etc */ }
object Client {
  @Resource("configuration_source") 
  private var config : ConfigSource = _ //Inject it at runtime  
  val clients = config.clients 
} 

这对我来说似乎是一个非常干净的解决方案(不是很多代码,意图明确),但var确实跳出来了(OTOH,在我看来这并不麻烦,因为我知道会被注入一次并且只有一次)。

在这种情况下,Reader monad 会是什么样子,像我 5 岁一样向我解释一下,它有什么优势?

让我们从你的方法和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)

为此,当然,您需要一些适当的定义 mapflatMap .我不会在这里讨论这个问题,但只想说Scalaz(或Rúnar的很棒的NEScala演讲,或者你已经看过的Tony的演讲)给你你需要的一切。

这里重要的一点是,ConfigSource依赖及其所谓的注入大多是隐藏的。我们在这里可以看到的唯一"提示"是ages属于Configured[(Int, Int, Int)]类型,而不仅仅是(Int, Int, Int) 。我们不需要在任何地方显式引用config

顺便说一句,这是我几乎总是喜欢思考monads的方式:它们隐藏了它们的效果,因此它不会污染代码的流,同时在类型签名中显式声明效果。 换句话说,你不需要重复太多:你在函数的返回类型中说"嘿,这个函数处理效果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 中,一切都可能由魔法管理,因此您并不真正知道代码的哪些部分可以在 DI 框架之外安全地重用(例如,在您的单元测试中,或者完全在其他项目中)。


更新:我写了一篇博客文章,更详细地解释了Reader

最新更新