Configuration data in Scala — should I use the Reader monad?

前端 未结 1 1188
渐次进展
渐次进展 2021-01-30 02:44

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

相关标签:
1条回答
  • 2021-01-30 02:57

    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.

    0 讨论(0)
提交回复
热议问题