scala - Reader Monad for Dependency Injection: multiple dependencies, nested calls -


when asked dependency injection in scala, quite lot of answers point using reader monad, either 1 scalaz or rolling own. there number of clear articles describing basics of approach (e.g. runar's talk, jason's blog), didn't manage find more complete example, , fail see advantages of approach on e.g. more traditional "manual" di (see the guide wrote). i'm missing important point, hence question.

just example, let's imagine have these classes:

trait datastore { def runquery(query: string): list[string] } trait emailserver { def sendemail(to: string, content: string): unit }  class findusers(datastore: datastore) {   def inactive(): unit = () }  class userreminder(finduser: findusers, emailserver: emailserver) {   def emailinactive(): unit = () }  class customerrelations(userreminder: userreminder) {   def retainusers(): unit = {} } 

here i'm modelling things using classes , constructor parameters, plays nicely "traditional" di approaches, design has couple of sides:

  • each functionality has enumerated dependencies. kind of assume dependencies needed functionality work properly
  • the dependencies hidden across functionalities, e.g. userreminder has no idea findusers needs datastore. functionalities can in separate compile units
  • we using pure scala; implementations can leverage immutable classes, higher-order functions, "business logic" methods can return values wrapped in io monad if want capture effects etc.

how modelled reader monad? retain characteristics above, clear kind of dependencies each functionality needs, , hide dependencies of 1 functionality another. note using classes more of implementation detail; maybe "correct" solution using reader monad use else.

i did find somewhat related question suggests either:

  • using single environment object dependencies
  • using local environments
  • "parfait" pattern
  • type-indexed maps

however, apart being (but that's subjective) bit complex such simple thing, in of these solutions e.g. retainusers method (which calls emailinactive, calls inactive find inactive users) need know datastore dependency, able call nested functions - or wrong?

in aspects using reader monad such "business application" better using constructor parameters?

how model example

how modelled reader monad?

i'm not sure if should modelled reader, yet can by:

  1. encoding classes functions makes code play nicer reader
  2. composing functions reader in comprehension , using it

just right before start need tell small sample code adjustments felt beneficial answer. first change findusers.inactive method. let return list[string] list of addresses can used in userreminder.emailinactive method. i've added simple implementations methods. finally, sample use following hand-rolled version of reader monad:

case class reader[conf, t](read: conf => t) { self =>    def map[u](convert: t => u): reader[conf, u] =     reader(self.read andthen convert)    def flatmap[v](toreader: t => reader[conf, v]): reader[conf, v] =     reader[conf, v](conf => toreader(self.read(conf)).read(conf))    def local[biggerconf](extractfrom: biggerconf => conf): reader[biggerconf, t] =     reader[biggerconf, t](extractfrom andthen self.read) }  object reader {   def pure[c, a](a: a): reader[c, a] =     reader(_ => a)    implicit def funtoreader[conf, a](read: conf => a): reader[conf, a] =     reader(read) } 

modelling step 1. encoding classes functions

maybe that's optional, i'm not sure, later makes comprehension better. note, resulting function curried. takes former constructor argument(s) first parameter (parameter list). way

class foo(dep: dep) {   def bar(arg: arg): res = ??? } // usage: val result = new foo(dependency).bar(arg) 

becomes

object foo {   def bar: dep => arg => res = ??? } // usage: val result = foo.bar(dependency)(arg) 

keep in mind each of dep, arg, res types can arbitrary: tuple, function or simple type.

here's sample code after initial adjustments, transformed functions:

trait datastore { def runquery(query: string): list[string] } trait emailserver { def sendemail(to: string, content: string): unit }  object findusers {   def inactive: datastore => () => list[string] =     datastore => () => datastore.runquery("select inactive") }  object userreminder {   def emailinactive(inactive: () => list[string]): emailserver => () => unit =     emailserver => () => inactive().foreach(emailserver.sendemail(_, "we miss you")) }  object customerrelations {   def retainusers(emailinactive: () => unit): () => unit =     () => {       println("emailing inactive users")       emailinactive()     } } 

one thing notice here particular functions don't depend on whole objects, on directly used parts. in oop version userreminder.emailinactive() instance call userfinder.inactive() here calls inactive() - function passed in first parameter.

please note, code exhibits 3 desirable properties question:

  1. it clear kind of dependencies each functionality needs
  2. hides dependencies of 1 functionality another
  3. retainusers method should not need know datastore dependency

modelling step 2. using reader compose functions , run them

reader monad lets compose functions depend on same type. not case. in our example findusers.inactive depends on datastore , userreminder.emailinactive on emailserver. solve problem 1 introduce new type (often referred config) contains of dependencies, change functions depend on , take relevant data. wrong dependency management perspective because way make these functions dependent on types shouldn't know in first place.

fortunately turns out, there exist way make function work config if accepts part of parameter. it's method called local, defined in reader. needs provided way extract relevant part config.

this knowledge applied example @ hand that:

object main extends app {    case class config(datastore: datastore, emailserver: emailserver)    val config = config(     new datastore { def runquery(query: string) = list("john.doe@fizzbuzz.com") },     new emailserver { def sendemail(to: string, content: string) = println(s"sending [$content] $to") }   )    import reader._    val reader = {     getaddresses <- findusers.inactive.local[config](_.datastore)     emailinactive <- userreminder.emailinactive(getaddresses).local[config](_.emailserver)     retainusers <- pure(customerrelations.retainusers(emailinactive))   } yield retainusers    reader.read(config)()  } 

advantages on using constructor parameters

in aspects using reader monad such "business application" better using constructor parameters?

i hope preparing answer made easier judge in aspects beat plain constructors. yet if enumerate these, here's list. disclaimer: have oop background , may not appreciate reader , kleisli don't use them.

  1. uniformity - no mater how short/long comprehension is, it's reader , can compose instance, perhaps introducing 1 more config type , sprinkling local calls on top of it. point imo rather matter of taste, because when use constructors nobody prevents compose whatever things like, unless stupid, doing work in constructor considered bad practice in oop.
  2. reader monad, gets benefits related - sequence, traverse methods implemented free.
  3. in cases may find preferable build reader once , use wide range of configs. constructors nobody prevents that, need build whole object graph anew every config incoming. while have no problem (i prefer doing on every request application), isn't obvious idea many people reasons may speculate about.
  4. reader pushes towards using functions more, play better application written in predominantly fp style.
  5. reader separates concerns; can create, interact everything, define logic without providing dependencies. supply later, separately. (thanks ken scrambler point). heard advantage of reader, yet that's possible plain constructors.

i tell don't in reader.

  1. marketing. impression, reader marketed kind of dependencies, without distinction if that's session cookie or database. me there's little sense in using reader practically constant objects, email server or repository example. such dependencies find plain constructors and/or partially applied functions way better. reader gives flexibility can specify dependencies @ every call, if don't need that, pay tax.
  2. implicit heaviness - using reader without implicits make example hard read. on other hand, when hide noisy parts using implicits , make error, compiler give hard decipher messages.
  3. ceremony pure, local , creating own config classes / using tuples that. reader forces add code isn't problem domain, therefore introducing noise in code. on other hand, application uses constructors uses factory pattern, outside of problem domain, weakness isn't serious.

what if don't want convert classes objects functions?

you want. technically can avoid that, happen if didn't convert findusers class object. respective line of comprehension like:

getaddresses <- ((ds: datastore) => new findusers(ds).inactive _).local[config](_.datastore) 

which not readable, that? point reader operates on functions, if don't have them already, need construct them inline, isn't pretty.


Comments

Popular posts from this blog

c++ - Delete matches in OpenCV (Keypoints and descriptors) -

java - Could not locate OpenAL library -

sorting - opencl Bitonic sort with 64 bits keys -