ThrushCond is not a Monad

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;DRview 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:


_18
case 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
_18
val userId: Int = 1
_18
val userName: Option[String] = Some("devth")
_18
val address: Option[String] = None
_18
val 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.


_11
type Step[A] = (A => Boolean, A => A)
_11
_11
case 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:


_10
val 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
_10
val request = requestPipeline run Request("/users")
_10
_10
//=>
_10
Request(/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:


_10
def 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.:


_10
val step: Request => Request =
_10
guard({_ => userName.isDefined}, {setParam(_, "userName", userName.get)})
_10
Some(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:


_10
def 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:


_15
case class F(x: Int)
_15
val f = F(10)
_15
val always = Function.const(true) _
_15
_15
val mult2: F => F = guard(always, {f => f.copy(x = f.x * 2)})
_15
val sub4: F => F = guard(always, {f => f.copy(x = f.x - 4)})
_15
val sub6: F => F = guard(always, {f => f.copy(x = f.x - 6)})
_15
_15
val g: (F => F) = (mult2 andThen sub6) andThen sub4
_15
val h: (F => F) = mult2 andThen (sub6 andThen sub4)
_15
_15
g(f)
_15
//=> F(10)
_15
h(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:


_10
import scalaz._, Scalaz._
_10
_10
case 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]:


_16
import ThrushCond.thrushCondSemigroup
_16
_16
val addPipeline = ThrushCond[Int](
_16
((_ > 10), (_ + 2)),
_16
((_ < 20), (_ + 20)))
_16
_16
val multPipeline = ThrushCond[Int](
_16
((_ == 70), (_ * 10)),
_16
((_ > 0), (_ * 7)))
_16
_16
val pipeline = addPipeline |+| multPipeline
_16
_16
// Examples
_16
multPipeline run 70 //=> 70 * 10 * 7 == 4900
_16
pipeline run 2 //=> (2 + 20) * 7 == 154
_16
pipeline 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, but List (which means that List[A])

To provide evidence of a PlusEmpty, we must be able to implement these two methods (where F):


_10
def plus[A](a: F[A], b: => F[A]): F[A] // from Plus
_10
def 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.


_19
case 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:


_18
import ThrushCond._ // evidence
_18
_18
val userPipeline = ThrushCond[Request](
_18
({_ => userName.isDefined}, {_.addParam("userName", userName.get)}),
_18
({_ => address.isDefined}, {_.addParam("address", address.get)}))
_18
_18
val headerPipeline = ThrushCond[Request](
_18
({_.isValidAccept(accept)}, {req =>
_18
req.addHeader("accept", req.acceptMap(accept))}))
_18
_18
// <+> is an alias for plus
_18
val requestPipeline = userPipeline <+> headerPipeline
_18
// A PlusEmpty[ThrushCond] is implicitly obtained and used to plus the two
_18
// ThrushCond[Request]s
_18
_18
requestPipeline run Request("/users")
_18
//=>
_18
Request(/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:


_13
import scala.language.postfixOps
_13
_13
val shouldCache = false
_13
_13
val cachePipeline = ThrushCond[Request](
_13
({_ => !shouldCache}, {_.addHeader("cache-control", "no-cache")}))
_13
_13
val requestPipeline = List(userPipeline, headerPipeline, cachePipeline) suml
_13
requestPipeline run Request("/users")
_13
//=>
_13
Request(/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.

View the full code listing.

Updated July 1, 2015: incorporated lmm's PlusEmpty suggestion.