Skip to content

Instantly share code, notes, and snippets.

@aappddeevv
Last active December 23, 2015 04:59
Show Gist options
  • Save aappddeevv/6583717 to your computer and use it in GitHub Desktop.
Save aappddeevv/6583717 to your computer and use it in GitHub Desktop.

##The Reader and State Monad The Reader and State monads are very similar in that they both wrap a function.

As we have seen in past posts, the State monad allows you to define a wrapper around a function that takes a current state and produces a tuple of the new state and a value. Returning a new state allows you to modify the state object for the next function that gets called. The state object used is immutable so when you return a new state object it is typically a copy of the incoming state with some type of adjustment e.g. increment a counter.

The Reader monad also wraps a function. However, it is designed to be a read-only mechanism in that the return value of the function it wraps is just a value. The reader uses a "context" concept instead of a "state" concept but they are essentially the same thing--an object of some type. However, since you only return a value from the wrapped reader function, the Reader communicates the concept that the context is immutable and not be changed. That's why its termed read-only.

Both the Reader and State monads are designed to input some type of object (context or state) that you extract a value from, use it in a function then produce a value. Conceptually, they are the same. Their usage is also very similar although Reader has fewer functions to support creating a mutated state or putting and getting a state in a for-comprehension because the context in the Reader is thought to be the same for all function calls in the sequence.

We also see that since the Reader (in scalaz) is just a function that takes a context and returns a value, we can also use the Reader to access a field in a decoupled way. Just like a Lens that abstracts the ability to obtain a value behind an uniform API and an arbitrary object, the Reader takes one object and returns a value. So we can use a Reader to not only create a larger computation that takes a context and returns a value but we can also use to access an individual field (think def myReturnAnIntFromTheContext = Reader[MyContext, Int](c => c.myInt)).

But is this the right way to use the Reader? After all, if our context is another object that accesses type-safe fields behind a def definition, could we just use the context object itself and the computation behind the def to abstract accessing configuration variable? Do we need both layers of abstraction and decoupling?

Also, what if we need a writable type context that a function can push a value into to be used downstream in the processing sequence?

##Typical Java Component Configuration Approaches

If ignore spring DI for the moment, a common approach to configuring a component is to pass it a map Map<String,Object> of parameters. There may be a wrapper around the map so you can return typed values e.g. config.getLong("myParameter") or config.getLongOrDefault("myParameter", 1000). When we look at Reader, we see that we have typed access through defs, vars or vals inside the context object. We also have, if Option is used, an easy way to handle missing configuration information. And we could lift a function to accept multiple Readers for the parameters. Since a Reader could return an Option value, the composed function would execute if all the configuration parameters are available or fail gracefully if they are not. Compared to java map-based configuration approaches, the Reader is not so bad.

The Reader is also designed to be statically checked. When using a map that takes a configuration name and returns a value, we often need those accessors to provide type casting and this usually needs a dynamic check inside the map'ish configuration object (the context in Reader's lingo). But does this mean the java map'ish approach is less safe? Not really. One could argue that at some point, the scala approach also has to read some persisted configuration information and some type of cast is needed at some layer of the software. With scala the typecast may be at a much lower level of the system then the java map'ish approach. This may be desired by some programmers. But anytime data comes for the external environment, there is some flavor of typecast somewhere going on. Of course, keeping that far away from our code is desirable.

When we use the reader, we create a function that takes a context and returns a value. The type Reader conveys this signature. The returned value could be Unit. The function we create, which returs a reader, implements or wraps the real function of interest.

The reason this model is seen as dependency injection is because the function we return wrapped in a Reader object takes the context object as an input variable. In this way, the function we really want to execute is given, without it having to do any work, the context object--the context is provided to the function by the external world. This is technically a form of dependency injection.

In spring DI, we would normally just annotate a field with @Autowired. That seems easier?!?! The reason that spring seems to easy compared to the scala approach of function composition is because we use a simple annotation to indicate which value should be injected when creating an instance of a class. But typing an annotation versus say returning a function have similar complexity when writing the function. And the spring DI approach is assuming an OO world of objects. To functional programmers, both approaches are just as readable although to java programmers, the functional approach with reader is probably much, much harder to read since the pattern will be unfamiliar.

So is it easier with spring DI and annotations or scala Reader? That depends on the specific programmer as well as the nature of the configuration problem.

##Field Level Access

Should we use the Reader for field level access? It does not really matter actually. If you only need one piece of configuration information from the context for your function, its probably a wash. If you need several, then there is not much overhead in designing your function to use the context object and you get the opportunity to compose it together with other functions using for-comprehensions and other scala functional features. If you only have a few objects to configure and the amount of configuration information is not large, then its all a wash and its probably easier just to pass around a context object in a global variable or directly as a parameter to the function. If you are using for-comprehensions or compose functions, then you are programming in a more functional style and it could make more sense to use Reader.

So should Reader be used just to access a single field? You can do that. You would want to use this approach when you need to provide a function to return configuration value and pass that to another object to execute in its workflow. However, its probably easier in this case to use structural typing (with a simple get def defined) or something that is not so coupled to the Reader type. You can also use the Reader when you want to lift a function to read the parameters from an external environment and use this function in place of the original function in your system.

You could use the Reader for simple field level access to your context, but there is not as much value in doing so then using it for larger sets of configuration information. Here's the trivial example:

val myIntParameterReader = Reader[MyContext, Int](c => c.myIntParameter)

Technically this fits the model of the Reader but it seems very fine grained versus a context object in the Reader that has many members in it representing many parameters together. The nice thing about this style is that it very efficient and very much like a scalaz Lens so there is no harm in using this type of value to pass around the ability to obtain a value without knowing where its from. This form also looks a little like the State monad--pass in a state and return a value (in the State monad case its the context+value tuple).

This fine-grained usage compares to a larger processing definition:

def bigProcessingFunc = Reader[MyContext, Option[BigResult]] ( context => ... }

However, simply doing def bigProcessingFunc(context: MyContext): Option[BigResult] seems like it would have been just as clear although it is clear less flexible for composition using other scala features.

##Example Scala Code The code below shows the use of the Reader class. One quick note. You can define your functions to explicitly return an object of type Reader[Context, Value] however since the implicits may be available in scalaz, you could also just as easily create a function with the actual function signature e.g. def yourFunc: Context => Int = { ... your func code... } and have the implicits convert it to the Reader object.

##But I Can't Store Anything I Want in the Context Actually, you can. But if you want as much type safety as you can get, you have to make the "fields" either vals, vars or def's. That way, you can attach the type information directly to the value of interest. If you just use a traditional java map'ish data structure, you only enforce types through the API through user specified type information e.g. getLong(propertyName) or get(propertyName, Long.TYPE)). With the approach described above, and even in java code, the creator of the context specifies the type.

import scalaz._

trait AppConfig {
  def tempFile: String
  def maxIterations: Int
  def outputDir: String
  def defaultServer: String
}

// This just returns an individual field in the config.
// This really looks like a lens since we are accessing
// a specific field in the config.
def tempFileReader = Reader[AppConfig, String](c => c.tempFile)
  
// This just returns an individual field in the config.
// This really looks like a lens since we are accessing a
// specific field in the the config.
def maxIterationsReader = Reader[AppConfig, String](c => c.defaultServer)
    
// Consider this larger processing function that does something
// more significant than just access a field.
def process1(config: AppConfig): Int = {
  println("maxIterations: " + config.maxIterations)
  config.maxIterations + 10
}
  
// See the comment for process1.
def process2(config: AppConfig): String = {
  println("defaultServer: " + config.defaultServer)
  config.defaultServer + "(very local)"
}

// A quicky config object with some hard-coded constants.
case object config1 extends AppConfig {
  override def tempFile = "output.tmp"
  override def maxIterations = 10
  override def outputDir = "."
  override def defaultServer = "localhost"
}

println("processing results: " + (process1(config1), process2(config1)))
println("The tempFile from the config: " + config1.tempFile)
println("The tempFile from the tempFileReader: " + tempFileReader(config1))

// Create some functions like process1 and process2 but use reader directly.
def process1r(delta: Int) = Reader[AppConfig, Int] { c =>
  println("maxIterations: " + c.maxIterations)
  c.maxIterations + delta
}

def process2r(arg: String) = Reader[AppConfig, String] { c =>
  println("defaultServer: " + c.defaultServer)
  c.defaultServer + "(" + arg + ")"
}

// Now we can use a for-comprehension to run our processes
val processr = for { 
  x <- process1r(10)
  y <- process2r("very local")
} yield (x, y)

println("processr result: " + processr(config1))

// Define a State for use in the next for-comprehension.
def doitWithState(arg: String) = State[AppConfig, String](c => (c, arg + "-" + c.maxIterations))

implicit def contextToState(c: AppConfig) = State.init[AppConfig]

// We can also convert the "context" to a State but you have to 
// wactch out where you do the conversion since you must respect
// the inferred type through the flatMap and map functions.
val processr2 = for {
  _ <- process1r(10).state
  z <- doitWithState("prefix")
} yield z

println("processr2 result: " + processr2(config1))

/** Prints
maxIterations: 10
defaultServer: localhost
processing results: (20,localhost(very local))
The tempFile from the config: output.tmp
The tempFile from the tempFileReader: output.tmp
maxIterations: 10
defaultServer: localhost
processr result: (20,localhost(very local))
maxIterations: 10
processr2 result: (config1,prefix-10)
**/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment