Skip to content

Instantly share code, notes, and snippets.

@mrange
Last active October 16, 2017 06:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mrange/8c3537df01a138429fc5adc2b026c231 to your computer and use it in GitHub Desktop.
Save mrange/8c3537df01a138429fc5adc2b026c231 to your computer and use it in GitHub Desktop.

Transformer combinators

I was considering the excerise and reworked it into that an account holds a bag of properties. The simplest approach is to use a map:

case class Account(props: Map<String, Any>)

However, this can be improved somewhat by introducing typed properties:

case class PropertyBag(properties: Map[String, Any]) {
  def get[A](property: Property[A]) : PropertyGetResult[A] = {
    val name = property.name
    properties.get(name) match {
      case Some(v: A)   => PropertyGetResult.HasValue[A](v)
      case Some(v)      => PropertyGetResult.TypeMismatch[A](name, v.getClass)
      case None         => PropertyGetResult.MissingValue[A](name)
    }
  }
  def set[A](property: Property[A], v: A): PropertyBag = {
    val name = property.name
    new PropertyBag(properties.updated (name, v))
  }
}

case class Property[+A](name: String, defaultValue: A)

This allows us to create a model of type properties:

object Properties {
  object CreditCard {
    val number      = property("P_CC__NUMBER"     , ""          )
    val expireDate  = property("P_CC__EXPIRE_DATE", Instant.MIN )
    val name        = property("P_CC__NAME"       , ""          )
    val cvc         = property("P_CC__CVC"        , ""          )
  }
}

A problem with this approach is how do we handle partial availability of data. Traditional error handling throws on the first missing data but I find that unsatisfactory. Instead I would like to have some way to collect all properties and all missing properties.

One way is to introduce a PropertyGetter function:

sealed abstract class PropertyGetterTree
object PropertyGetterTree {
  case class Empty()                                                    extends PropertyGetterTree
  case class Leaf (failure: PropertyGetterFailure)                      extends PropertyGetterTree
  case class Fork (left: PropertyGetterTree, right: PropertyGetterTree) extends PropertyGetterTree
}

case class PropertyGetterResult[+A](value: A, tree: PropertyGetterTree)

case class PropertyGetter[+A](f: PropertyBag => PropertyGetterResult[A])

The idea here is that the PropertyGetter function applied to a property bad returns a value and potential errors while producing that value in the form of a tree. If no error were detected the the tree is empty.

This approach will allow for collection all errors while producing a value.

This function can be made into a Monad and Applicative for succinct composition:

// b is a property getter that extracts credit card info
val b =
    unit(CreditCardInfo.curried)  <*>
    get(number)                   <*>
    get(expireDate)               <*>
    get(name)                     <*>
    get(cvc)

// Sets up a property bag with credit card info
val pb =
    PropertyBag.empty
        .set(number     , "1234")
        .set(expireDate , Instant.now())
        .set(name       , "Bill Gates")
        .set(cvc        , "123")

// Extract credit card info
val gr = PropertyGetter.run(b, pb)
println(s"Good result: $gr")

// Try to extract credit card info from empty bag,
//  will fail
val br = PropertyGetter.run(b, PropertyBag.empty)
println(s"Bad result: $br")

Full source code in the same gist.

I have take this pattern to a more complete solution here (F#): https://gist.github.com/mrange/18ca0863c45a3c00a670afb09379d4c1

// -----------------------------------------------------------------------------
// PropertyBag is an immutable map of typed properties
// -----------------------------------------------------------------------------
sealed abstract class PropertyGetResult[+A]
object PropertyGetResult {
case class HasValue[+A] (value: A) extends PropertyGetResult[A]
case class MissingValue[+A] (name: String) extends PropertyGetResult[A]
case class TypeMismatch[+A] (name: String, cls: Class[_]) extends PropertyGetResult[A]
}
case class PropertyBag(properties: Map[String, Any]) {
def get[A](property: Property[A]) : PropertyGetResult[A] = {
val name = property.name
properties.get(name) match {
case Some(v: A) => PropertyGetResult.HasValue[A](v)
case Some(v) => PropertyGetResult.TypeMismatch[A](name, v.getClass)
case None => PropertyGetResult.MissingValue[A](name)
}
}
def set[A](property: Property[A], v: A): PropertyBag = {
val name = property.name
new PropertyBag(properties.updated (name, v))
}
}
object PropertyBag {
val empty = new PropertyBag(Map.empty)
}
case class Property[+A](name: String, defaultValue: A)
object Property {
def property[A](name: String, defaultValue: A): Property[A] = Property[A](name, defaultValue)
}
import Property._
// -----------------------------------------------------------------------------
// PropertyGetter is a function that given a property bag extracts a value
// possibly a combination of many properties
// -----------------------------------------------------------------------------
sealed abstract class PropertyGetterFailure
object PropertyGetterFailure {
case class MissingValue (name: String) extends PropertyGetterFailure
case class TypeMismatch (name: String, cls: Class[_]) extends PropertyGetterFailure
}
// The property getter accumulates all errors as a tree
sealed abstract class PropertyGetterTree
object PropertyGetterTree {
case class Empty() extends PropertyGetterTree
case class Leaf (failure: PropertyGetterFailure) extends PropertyGetterTree
case class Fork (left: PropertyGetterTree, right: PropertyGetterTree) extends PropertyGetterTree
val empty = Empty()
def leaf(f: PropertyGetterFailure): PropertyGetterTree =
Leaf(f)
def fork(l: PropertyGetterTree, r: PropertyGetterTree): PropertyGetterTree =
l match {
case _: Empty => r
case _ => r match {
case _: Empty => l
case _ => Fork(l, r)
}
}
}
case class PropertyGetterResult[+A](value: A, tree: PropertyGetterTree)
object PropertyGetterResult {
import PropertyGetterTree._
def result[A] (value: A, tree: PropertyGetterTree) : PropertyGetterResult[A] = PropertyGetterResult[A](value, tree)
def good[A] (value: A) : PropertyGetterResult[A] = result(value, empty)
def bad[A] (value: A, failure: PropertyGetterFailure): PropertyGetterResult[A] = result(value, leaf(failure))
}
case class PropertyGetter[+A](f: PropertyBag => PropertyGetterResult[A]) {
def bind[B](bf: A => PropertyGetter[B]): PropertyGetter[B] = PropertyGetter.bind(this)(bf)
def >>=[B](bf: A => PropertyGetter[B]): PropertyGetter[B] = PropertyGetter.bind(this)(bf)
}
object PropertyGetter {
import PropertyGetterResult._
import PropertyGetterTree._
def unit[A](v: A): PropertyGetter[A] =
PropertyGetter[A](pb => good(v))
def bind[A,B](a: PropertyGetter[A])(bf: A => PropertyGetter[B]): PropertyGetter[B] =
PropertyGetter[B](pb => {
val ar = a.f(pb)
val b = bf(ar.value)
val br = b.f(pb)
result(br.value, fork(ar.tree, br.tree))
})
def ap[A,B](f: PropertyGetter[A => B])(a: PropertyGetter[A]): PropertyGetter[B] =
PropertyGetter[B](pb => {
val fr = f.f(pb)
val ar = a.f(pb)
result(fr.value(ar.value), fork(fr.tree, ar.tree))
})
def get[A](p: Property[A]): PropertyGetter[A] =
PropertyGetter[A](pb => {
pb.get(p) match {
case hv: PropertyGetResult.HasValue[A] => good(hv.value)
case mv: PropertyGetResult.MissingValue[A] => bad(p.defaultValue, PropertyGetterFailure.MissingValue(mv.name))
case tm: PropertyGetResult.TypeMismatch[A] => bad(p.defaultValue, PropertyGetterFailure.TypeMismatch(tm.name, tm.cls))
}
})
def run[A](a: PropertyGetter[A], pb: PropertyBag) =
a.f(pb)
}
object Implicits {
implicit class ApPropertyGetter[A,B](f: PropertyGetter[A => B]) {
def <*>(a: PropertyGetter[A]): PropertyGetter[B] = PropertyGetter.ap(f)(a)
}
}
import Implicits._
import java.time.Instant
// Define some properties related to credit card
object Properties {
object CreditCard {
val number = property("P_CC__NUMBER" , "" )
val expireDate = property("P_CC__EXPIRE_DATE", Instant.MIN )
val name = property("P_CC__NAME" , "" )
val cvc = property("P_CC__CVC" , "" )
}
}
// Defines a holder for a credit card
case class CreditCardInfo(number: String, expireDate: Instant, name: String, cvc: String)
object Main extends App {
import PropertyGetter._
import Properties.CreditCard._
// b is a property getter that extracts credit card info
val b =
unit(CreditCardInfo.curried) <*>
get(number) <*>
get(expireDate) <*>
get(name) <*>
get(cvc)
// Sets up a property bag with credit card info
val pb =
PropertyBag.empty
.set(number , "1234")
.set(expireDate , Instant.now())
.set(name , "Bill Gates")
.set(cvc , "123")
// Extract credit card info
val gr = PropertyGetter.run(b, pb)
println(s"Good result: $gr")
// Try to extract credit card info from empty bag,
// will fail
val br = PropertyGetter.run(b, PropertyBag.empty)
println(s"Bad result: $br")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment