【问题标题】:Configuration data in Scala -- should I use the Reader monad?Scala 中的配置数据——我应该使用 Reader monad 吗?
【发布时间】:2012-06-30 12:07:18
【问题描述】:

如何在 Scala 中创建功能正常的可配置对象?我在Reader monad 上观看了 Tony Morris 的视频,但我仍然无法将这些点联系起来。

我有一个硬编码的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 岁一样向我解释它,它有什么优势?

【问题讨论】:

  • vals 可以使用反射进行修改,因此您的依赖注入库可能会“注入 val”
  • @gerferra 如果我们有 var,那么通过反射修改 val 的意义何在?
  • 为什么不让Client 成为一个带参数的类,这样配置就可以传递给Client 的实例?
  • @matt 对,这将是另一种方法,但这仍然让我不清楚是否/为什么首选 Reader monad。
  • 您可能会发现Rúnar's NEScala talk 的前半部分更平易近人。

标签: scala configuration monads reader-monad


【解决方案1】:

让我们从您的方法与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's awesome NEScala talk,或您已经看到的Tony's)为您提供了您所需要的一切。

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

顺便说一句,这是我几乎总是喜欢思考 monad 的方式:它们隐藏其效果,因此它不会污染您的代码流,而 在类型签名中明确声明效果。换句话说,你不必过多地重复自己:你在函数的返回类型中说“嘿,这个函数处理 effect X”,不要再搞砸了。 p>

在这个例子中,效果当然是从某个固定的环境中读取。您可能熟悉的另一个一元效应包括错误处理:我们可以说Option 隐藏了错误处理逻辑,同时在您的方法类型中明确显示错误的可能性。或者,与阅读相反,Writer monad 隐藏了我们正在写入的内容,同时在类型系统中显式显示它的存在。

最后,就像我们通常需要引导一个 DI 框架(在我们通常的控制流之外的某个地方,例如在 XML 文件中)一样,我们还需要引导这个奇怪的 monad。当然,我们的代码会有一些逻辑入口点,例如:

def run: Configured[Unit] = // ...

结果很简单:因为Configured[A] 只是函数ConfigSource =&gt; A 的类型同义词,我们可以将函数应用到它的“环境”:

run(ConfigFileSource)
// or
run(DatabaseSource)

哒哒!因此,与传统的 Java 风格的 DI 方法相比,这里没有任何“魔法”发生。可以说,唯一的魔力是封装在 Configured 类型的定义中,以及它作为 monad 的行为方式。最重要的是,类型系统让我们诚实知道发生在哪个“领域”依赖注入:类型为 Configured[...] 的任何东西都在 DI 世界中,而没有它的任何东西都不是。我们根本无法在老式 DI 中做到这一点,其中 一切 都可能由魔法管理,因此您并不真正知道代码的哪些部分可以安全地在 DI 框架之外重用(例如,在您的单元测试中,或完全在其他项目中)。


更新:我写了一个blog post,更详细地解释了Reader

【讨论】:

  • 刚刚想到,我也应该说:不要担心制作“可配置对象”。 可配置对象实际上只是带有构造函数参数的东西。这些参数从何而来?当然,构造函数的调用者会反过来(如果我说服你尝试的话)从读者那里获取它们(在本例中为Configured[...] 环境)。这都是关于调用其他函数的函数,而不是关于对象的内部结构。
  • 嗯...所以最终我们仍然必须重构我们所有的 fn 签名,以便它们返回 Configured[PriorReturnedType] 直到我们选择 run(ConfigFileSource)run(DatabaseSource) 的 fn?为什么这优于将 ConfigSource 作为 arg 传递?而且我不理解“我们这里没有任何“魔法”发生。”我们仍然必须通过命令行参数或环境变量或 DI 的“魔法”来选择 run(ConfigFileSource)run(DatabaseSource),不是吗?
  • re:“所以最终我们仍然必须重构我们所有的 fn 签名......” - 其中一些,不是全部。任何依赖于全局配置的东西都需要Configured[...] 签名,但你的纯函数不需要。同样,保持这两个世界的可识别性是一件好事。
  • re: "magic" - 我可以轻松解释 Reader 的工作原理:它是一个函数,它需要一个参数。相比之下,Spring(例如)如何工作?好吧,你的系统中有一些你自己没有实例化的类;你让 Spring 为你实例化它们。 Spring 知道这一点,因为您已经配置了应用程序上下文。因此,您的应用程序上下文会告诉 Spring 在构造对象时应该调用哪些方法。但不是真的,他们必须遵循 bean 命名约定,这样它才能 derp derp derp 你明白了吗?
  • 关于 Reader Monad DI 如何与构造函数参数 DI 进行比较,以及如何将 RM 与多个依赖项/嵌套方法调用一起使用的相关问题:stackoverflow.com/questions/29174500/…
猜你喜欢
  • 2014-01-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多