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 ThrushCond
s 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
String
is 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 ThrushCond
s 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.