pipes

2014-06-04

I was looking for an operator to make the following easier to read:

val hans = Person("Herbert", "Fischer", new java.util.Date(now - (23 * MilliSecondsInYear)))
val withAge = addAge(hans)
val withAgeAndFullName = addFullName(withAge)

More specifically, the goal is to fill in optional fields based on initially populated fields. In this case the mandatory fields are firstName, lastName and birthDate and we derive the age and fullName:

val MilliSecondsInYear = 1000 * 60 * 60 * 24 * 365L
def now = System.currentTimeMillis

case class Person(
  firstName: String,
  lastName: String,
  birthDate: java.util.Date,
  age: Option[Long] = None,
  fullName: Option[String] = None)

def addFullName(person: Person) = person.copy(fullName = Some(s"${person.firstName} ${person.lastName}"))
def addAge(person: Person) = person.copy(age = Some((now - person.birthDate.getTime) / MilliSecondsInYear))

The full example I was looking at had about ten chained function invocations. Inserting intermediate results led to redundant names as in the example above and nesting the function invocations felt hard to read as you have to read the expressions from the inside out:

val withAgeAndFullName = addFullName(addAge(hans))

My preferred solution was imitating pipes like this:

ls | sort | uniq -c

So ideally we could write:

val withAgeAndFullName = hans | addAge | addFullName

But that's the bitwise OR. The alternative I've seen is the following:

val withAgeAndFullName = hans |> addAge |> addFullName

Rather than including a library for this, we can try this ourselves. First, we need to pimp objects with our operator:

class PimpAny[A](any: A) {
  def |>[B](fun: A ⇒ B): B = fun(any)
}

And add an implicit conversion to make this available:

implicit def pimpAnyWithPipeOperator[A](any: A) = new PimpAny(any)

We can be even more concise and combine the wrapping class and the implicit conversion via syntactic sugar in an implicit class:

implicit class PimpAnyWithPipeOperator[A](any: A) {
  def |>[B](fun: A ⇒ B): B = fun(any)
}

And we're done -- fun practice to learn about implicit classes. :)

Full REPL example with pipes:

implicit class PimpAnyWithPipeOperator[A](any: A) {
  def |>[B](fun: A ⇒ B): B = fun(any)
}

val MilliSecondsInYear = 1000 * 60 * 60 * 24 * 365L

case class Person(
  firstName: String,
  lastName: String,
  birthDate: java.util.Date,
  age: Option[Long] = None,
  fullName: Option[String] = None)

def now = System.currentTimeMillis

def addFullName(person: Person): Person =
  person.copy(fullName = Some(s"${person.firstName} ${person.lastName}"))

def addAge(person: Person): Person =
  person.copy(age = Some((now - person.birthDate.getTime) / MilliSecondsInYear))

val hans = Person("Herbert", "Fischer", new java.util.Date(now - (23 * MilliSecondsInYear)))
val withAgeAndFullName = hans |> addAge |> addFullName