Scala中的配置数据 - 我应该使用Reader monad吗? [英] Configuration data in Scala -- should I use the Reader monad?

查看:253
本文介绍了Scala中的配置数据 - 我应该使用Reader monad吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



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

  class Client(name:String,age:Int){/ * etc * /} 

客户端{
// Horrible!
val clients = List(Client(Bob,20),Client(Cindy,30))
}


我想要在运行时确定 Client.clients ,具有从属性文件或数据库读取的灵活性。在Java世界中,我定义一个接口,实现两种类型的源,并使用DI来分配一个类变量:

  trait ConfigSource {
def clients:List [Client]
}

对象ConfigFileSource扩展ConfigSource {
覆盖def clients = buildClientsFromProperties(属性(clients.properties ))
//...etc,读属性文件
}

对象DatabaseSource扩展ConfigSource {/ * etc * /}

对象客户端{
@Resource(configuration_source)
private var config:ConfigSource = _ //在运行时注入它

val clients = config.clients
}

这似乎是一个非常干净的解决方案(不是很多代码,明确的意图) code> var 跳出来(OTOH,它似乎并不真的麻烦,因为我知道它一次注入一次)。



阅读器 monad在这种情况下会是什么样子,它的优点是什么?

解决方案

让我们从您的方法和读者之间的简单,表面的区别开始方法,这是你不再需要挂在 config 在任何地方。让我们假设你定义了下面这个类型的同义词:

  type Configured [A] = ConfigSource => A 

现在,如果我需要一个 ConfigSource 对于某些函数,假设一个函数获取列表中的 n 个客户端,我可以将该函数声明为configured:

  def nthClient(n:Int):Configured [Client] = {
config => config.clients(n)
}

$ c> config 出了稀薄的空气,任何时候我们需要一个!闻起来像依赖注入,对吧?现在假设我们想要列表中第一,第二和第三个客户的年龄(假设它们存在):

  def年龄:Configured [(Int,Int,Int)] = 
for {
a0 < - nthClient(0)
a1 < - nthClient(1)
a2 < nthClient(2)
} yield(a0.age,a1.age,a2.age)

当然,你需要一些适当的定义 map flatMap 。我不会在这里,但会简单地说,Scalaz(或Rúnar的真棒NEScala谈话,或< a href =http://vimeo.com/20674558>您已经看过的托尼)给您所有需要的。



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



作为一个,这是我几乎总是喜欢考虑monads的方式:他们隐藏他们的效果,因此它不会污染你的代码流,而在类型签名中显式声明效果。换句话说,你不需要重复你太多:你说嘿,这个函数处理函数的返回类型中的效果X ,并且不要进一步混乱。 p>

在这个例子中,当然效果是从一些固定的环境中读取。你可能熟悉的另一个monadic效果包括错误处理:我们可以说选项隐藏错误处理逻辑,同时使你的方法的类型中显示错误的可能性。或者,阅读的反面, Writer monad隐藏了我们写的东西,同时使其在类型系统中显式。



现在最后,正如我们通常需要引导一个DI框架(在我们通常的控制流程之外, XML文件),我们还需要引导这个好奇的monad。当然我们将有一些逻辑的入口点到我们的代码,如:

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

最后非常简单:自 只是函数 ConfigSource =>的类型同义词。 A ,我们可以将该函数应用到其环境:

  run(ConfigFileSource)
//或
run(DatabaseSource)



<因此,与传统的Java式DI方法相比,我们没有任何魔法出现在这里。唯一的魔法,就像它是封装在我们的配置类型的定义和它的行为方式monad。最重要的是,类型系统让我们诚实地了解哪些领域依赖注入发生在: Configured [...] 在DI世界,没有它的东西不是。我们根本不会在老式DI中得到这些,因为 可能由魔法管理,所以你不知道你的代码的哪些部分可以安全地在DI框架之外重用(例如,在您的单元测试中,或完全在其他项目中)。






update: strong>我写了一个博客文章,其中解释了 Reader 在更大详情。


How do I create a properly functional configurable object in Scala? I have watched Tony Morris' video on the Reader monad and I'm still unable to connect the dots.

I have a hard-coded list of Client objects:

class Client(name : String, age : Int){ /* etc */}

object Client{
  //Horrible!
  val clients  = List(Client("Bob", 20), Client("Cindy", 30))
}

I want Client.clients to be determined at runtime, with the flexibility of either reading it from a properties file or from a database. In the Java world I'd define an interface, implement the two types of source, and use DI to assign a class variable:

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 
} 

This seems like a pretty clean solution to me (not a lot of code, clear intent), but that var does jump out (OTOH, it doesn't seem to me really troublesome, since I know it will be injected once-and-only-once).

What would the Reader monad look like in this situation and, explain it to me like I'm 5, what are its advantages?

解决方案

Let's start with a simple, superficial difference between your approach and the Reader approach, which is that you no longer need to hang onto config anywhere at all. Let's say you define the following vaguely clever type synonym:

type Configured[A] = ConfigSource => A

Now, if I ever need a ConfigSource for some function, say a function that gets the n'th client in the list, I can declare that function as "configured":

def nthClient(n: Int): Configured[Client] = {
  config => config.clients(n)
}

So we're essentially pulling a config out of thin air, any time we need one! Smells like dependency injection, right? Now let's say we want the ages of the first, second and third clients in the list (assuming they exist):

def ages: Configured[(Int, Int, Int)] =
  for {
    a0 <- nthClient(0)
    a1 <- nthClient(1)
    a2 <- nthClient(2)
  } yield (a0.age, a1.age, a2.age)

For this, of course, you need some appropriate definition of map and flatMap. I won't get into that here, but will simply say that Scalaz (or Rúnar's awesome NEScala talk, or Tony's which you've seen already) gives you all you need.

The important point here is that the ConfigSource dependency and its so-called injection are mostly hidden. The only "hint" that we can see here is that ages is of type Configured[(Int, Int, Int)] rather than simply (Int, Int, Int). We didn't need to explicitly reference config anywhere.

As an aside, this is the way I almost always like to think about monads: they hide their effect so it's not polluting the flow of your code, while explicitly declaring the effect in the type signature. In other words, you needn't repeat yourself too much: you say "hey, this function deals with effect X" in the function's return type, and don't mess with it any further.

In this example, of course the effect is to read from some fixed environment. Another monadic effect you might be familiar with include error-handling: we can say that Option hides error-handling logic while making the possibility of errors explicit in your method's type. Or, sort of the opposite of reading, the Writer monad hides the thing we're writing to while making its presence explicit in the type system.

Now finally, just as we normally need to bootstrap a DI framework (somewhere outside our usual flow of control, such as in an XML file), we also need to bootstrap this curious monad. Surely we'll have some logical entry point to our code, such as:

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

It ends up being pretty simple: since Configured[A] is just a type synonym for the function ConfigSource => A, we can just apply the function to its "environment":

run(ConfigFileSource)
// or
run(DatabaseSource)

Ta-da! So, contrasting with the traditional Java-style DI approach, we don't have any "magic" occurring here. The only magic, as it were, is encapsulated in the definition of our Configured type and the way it behaves as a monad. Most importantly, the type system keeps us honest about which "realm" dependency injection is occurring in: anything with type Configured[...] is in the DI world, and anything without it is not. We simply don't get this in old-school DI, where everything is potentially managed by the magic, so you don't really know which portions of your code are safe to reuse outside of a DI framework (for example, within your unit tests, or in some other project entirely).


update: I wrote up a blog post which explains Reader in greater detail.

这篇关于Scala中的配置数据 - 我应该使用Reader monad吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆