Clojure has a useful macro called cond-> that conditionally threads an initial value through a series of predicate/function pairs only applying each function if its predicate returns true. In this post we're going to look at a Scala representation, and whether it fits the shape and laws of any common algebraic structures. We'll look at Functor, Monad, Semigroup, and Monoid.
TL;DR — view the full code listing.
Let's start with an example in Clojure. We want to build up a request based on some arbitrary conditions:
_28;; sample values from user input_28_28(def user-id 1)_28(def user-name "devth")_28(def user-address nil)_28(def accept :json)_28_28;; validation and helper functions_28_28(def accept-map )_28(defn valid-accept? [a] (accept-map a))_28(defn set-header [req k v] (update-in req [:headers] assoc k v))_28(defn set-param [req k v] (update-in req [:params] assoc k v))_28_28;; build up a request map using cond-> to decide which items to add to params_28;; and headers maps_28_28(def request_28 (cond->_28 user-name (set-param :user-name user-name)_28 user-address (set-param :user-address user-address)_28 (valid-accept? accept) (set-header :accept accept)))_28_28;; request value:_28_28{:target "/users"_28 :query-params_28 :headers
Since Clojure's -> operator is sometimes referred to as
the "thrush" operator, I'm going to call cond->
in Scala ThrushCond.
First let's model the Request and helpers equivalent to
those we used in the Clojure example:
_18case class Request(_18 target: String,_18 params: Map[String, String] = Map.empty,_18 headers: Map[String, String] = Map.empty) {_18_18 // validation and helper functions_18 val acceptMap = Map("html" -> "text/html", "json" -> "application/json")_18 val isValidAccept: (String => Boolean) = acceptMap.isDefinedAt __18_18 def addParam(k: String, v: String) = this.copy(params=params.updated(k, v))_18 def addHeader(k: String, v: String) = this.copy(headers=headers.updated(k, v))_18}_18_18// sample values from user input_18val userId: Int = 1_18val userName: Option[String] = Some("devth")_18val address: Option[String] = None_18val accept = "json"
Now we'll create the ThrushCond class that takes any number
of predicate/function pairs, provides a guard function to only run a function
if the predicate passes, a method to flatten the chain of functions via
composition, and finally a run method that takes a value and
runs it through the chain.
_11type Step[A] = (A => Boolean, A => A)_11_11case class ThrushCond[A](steps: Step[A]*) {_11 /** Perform a pipeline step only if the value meets a predicate */_11 def guard[A](pred: (A => Boolean), fn: (A => A)): (A => A) =_11 (a: A) => if (pred(a)) fn(a) else a_11 /** Compose the steps into a single function */_11 def comp = Function.chain(steps.map { step => guard(step._1, step._2) })_11 /** Run a value through the pipeline */_11 def run(a: A) = comp(a)_11}
Try it out:
_10val requestPipeline = ThrushCond[Request](_10 ({_ => userName.isDefined}, {_.addParam("userName", userName.get)}),_10 ({_ => address.isDefined}, {_.addParam("address", address.get)}),_10 ({_.isValidAccept(accept)}, {r => r.addHeader("accept", r.acceptMap(accept))}))_10_10val request = requestPipeline run Request("/users")_10_10//=>_10Request(/users,Map(userName -> devth),Map(accept -> application/json))
As you can see, it correctly skipped the 2nd step based on the
address.isDefined condition and runs the other steps because
their predicates evaluate to true.
Will this work as one of the algebraic structures mentioned at the start?
Functor
Consider Functor's fmap:
_10def fmap[A, B](f: A => B): F[A] => F[B]
In our case, both A are the
same type, Request produces a
function that fits, but we could easily use that with an existing Functor,
e.g.:
_10val step: Request => Request =_10 guard({_ => userName.isDefined}, {setParam(_, "userName", userName.get)})_10Some(Request("/users")).map(step)
The essense of ThrushCond
itself so it makes no sense to design a new Functor around it.
Monad
Likewise, Monad's flatMap:
_10def flatMap[A, B](f: A => F[B]): F[A] => F[B]
We could make guard's
signature, but there's no point in doing so for the same reason it didn't make
sense for Functor: the essense is not how a transformation is applied, it's
whether the transformation is applied, and because of the signature, the
decision whether to perform a transformation must be embedded in the
transformation itself, hence guard.
Semigroup
Let's see if it meets Semigroup's associativity laws:
_15case class F(x: Int)_15val f = F(10)_15val always = Function.const(true) __15_15val mult2: F => F = guard(always, {f => f.copy(x = f.x * 2)})_15val sub4: F => F = guard(always, {f => f.copy(x = f.x - 4)})_15val sub6: F => F = guard(always, {f => f.copy(x = f.x - 6)})_15_15val g: (F => F) = (mult2 andThen sub6) andThen sub4_15val h: (F => F) = mult2 andThen (sub6 andThen sub4)_15_15g(f)_15//=> F(10)_15h(f)_15//=> F(10)
guard is associative when composed with itself because
function composition is associative.
Because of this associative binary operation we can provide evidence that
ThrushCond's
Semigroup representation:
_10import scalaz._, Scalaz.__10_10case object ThrushCond {_10 /** Evidence of a Semigroup */_10 implicit def thrushCondSemigroup[A]: Semigroup[ThrushCond[A]] =_10 new Semigroup[ThrushCond[A]] {_10 def append(t1: ThrushCond[A], t2: => ThrushCond[A]): ThrushCond[A] =_10 ThrushCond[A]((Function.const(true), t2.comp compose t1.comp))_10 }_10}
We've defined a Semigroup over the set of all
ThrushCond[A]s. What does this give us? We can now combine
any number of ThrushConds using Semigroup's
|+| operator. A simple example using
ThrushCond[Int]:
_16import ThrushCond.thrushCondSemigroup_16_16val addPipeline = ThrushCond[Int](_16 ((_ > 10), (_ + 2)),_16 ((_ < 20), (_ + 20)))_16_16val multPipeline = ThrushCond[Int](_16 ((_ == 70), (_ * 10)),_16 ((_ > 0), (_ * 7)))_16_16val pipeline = addPipeline |+| multPipeline_16_16// Examples_16multPipeline run 70 //=> 70 * 10 * 7 == 4900_16pipeline run 2 //=> (2 + 20) * 7 == 154_16pipeline run 12 //=> (12 + 2 + 20) * 7 == 238
Monoid via PlusEmpty
Monoids are Semigroups with an identity element.
ThrushCond's identity is simply a
ThrushCond arguments.
However, as @lmm mentioned in the
comments:
it's not ThrushCond itself that forms a Monoid but rather ThrushCond[A] for any given A
This is where PlusEmpty comes in.
PlusEmpty is a "universally quantified
Monoid"
which means it's like a Monoid but for first-order * -> *
types instead of proper * types.
PlusEmpty itself is a higher-order (* -> *) -> * type. A
helpful quote from #scalaz:
tpolecat: so
Stringis a monoid, butList(which means thatList[A])
To provide evidence of a PlusEmpty, we must be able to implement these two
methods (where F):
_10def plus[A](a: F[A], b: => F[A]): F[A] // from Plus_10def empty[A]: F[A] // from PlusEmpty which extends Plus
We already implemented plus for Semigroup's append, and
empty is simply a ThrushCond without args.
_19case object ThrushCond {_19 /** Evidence of a PlusEmpty */_19 implicit def thrushCondPlusEmpty: PlusEmpty[ThrushCond] =_19 new PlusEmpty[ThrushCond] {_19 def plus[A](a: ThrushCond[A], b: => ThrushCond[A]): ThrushCond[A] =_19 ThrushCond[A((Function.const(true), b.comp compose a.comp)))_19_19 def empty[A]: ThrushCond[A] = ThrushCond[A]()_19 }_19 /** Use PlusEmpty to provide evidence of a Monoid[Request] */_19 implicit def requestMonoid: Monoid[ThrushCond[Request]] =_19 thrushCondPlusEmpty.monoid[Request]_19 /** Evidence of a Semigroup */_19 implicit def thrushCondSemigroup[A]: Semigroup[ThrushCond[A]] =_19 new Semigroup[ThrushCond[A]] {_19 def append(t1: ThrushCond[A], t2: => ThrushCond[A]): ThrushCond[A] =_19 ThrushCond[A]((Function.const(true), t2.comp compose t1.comp))_19 }_19}
Let's go back to our Request example in Clojure and use
PlusEmpty's <+> to combine separate transformation
pipelines:
_18import ThrushCond._ // evidence_18_18val userPipeline = ThrushCond[Request](_18 ({_ => userName.isDefined}, {_.addParam("userName", userName.get)}),_18 ({_ => address.isDefined}, {_.addParam("address", address.get)}))_18_18val headerPipeline = ThrushCond[Request](_18 ({_.isValidAccept(accept)}, {req =>_18 req.addHeader("accept", req.acceptMap(accept))}))_18_18// <+> is an alias for plus_18val requestPipeline = userPipeline <+> headerPipeline_18// A PlusEmpty[ThrushCond] is implicitly obtained and used to plus the two_18// ThrushCond[Request]s_18_18requestPipeline run Request("/users")_18//=>_18Request(/users,Map(userName -> devth),Map(accept -> application/json))
Because PlusEmpty can derive a Monoid for a given type, we can combine any
number of ThrushConds from a List. Let's construct one more
ThrushCond pipeline that conditionally adds a cache-control
header and try out our Monoid using Foldable's
suml:
_13import scala.language.postfixOps_13_13val shouldCache = false_13_13val cachePipeline = ThrushCond[Request](_13 ({_ => !shouldCache}, {_.addHeader("cache-control", "no-cache")}))_13_13val requestPipeline = List(userPipeline, headerPipeline, cachePipeline) suml_13requestPipeline run Request("/users")_13//=>_13Request(/users,_13 Map(userName -> devth),_13 Map(accept -> application/json, cache-control -> no-cache))
ThrushCond is not a Monad, nor a Functor, but it is a PlusEmpty from which can be derived a Monoid.
Updated July 1, 2015: incorporated lmm's PlusEmpty suggestion.