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

时间:2012-06-28 22:11:20

标签: scala configuration monads reader-monad

如何在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岁那样向我解释,它的优点是什么?

1 个答案:

答案 0 :(得分:46)

让我们从您的方法与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 talkTony's)可以为你提供所需的一切。

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

  
    

作为一个,这是我几乎总是喜欢考虑monad的方式:他们隐藏他们的效果所以它不会污染你的代码流,而< strong>在类型签名中明确声明效果。换句话说,你不需要重复太多:你说“嘿,这个函数在函数的返回类型中处理效果X ”,并且不要再乱用它了。

         

在这个例子中,当然效果是从一些固定的环境中读取。你可能熟悉的另一个monadic效应包括错误处理:我们可以说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框架之外重用(例如,在您的单元测试中,或完全在其他项目中)。


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