Debasish specializes in leading delivery of enterprise scale solutions for various clients ranging from small ones to Fortune 500 companies. He is the technology evangelist of Anshin Software (http://www.anshinsoft.com) and takes pride in institutionalizing best practices in software design and programming. He loves to program in Java, Ruby, Erlang and Scala and has been trying desperately to get out of the unmanaged world of C++. Debasish is a DZone MVB and is not an employee of DZone and has posted 55 posts at DZone. You can read more from them at their website. View Full User Profile

Composable Domain Models Using Scalaz

12.02.2010
| 4026 views |
  • submit to reddit

I have been having some solid fun working through scalaz - it's possibly as close you can get to Haskell with a postfunctional language like Scala, which also supports object oriented paradigms. One of the ways I do learn languages is by developing domain models using the idioms that the language offers and try to make the model as expressive as possible. I pick up domains on which I have worked before - so I have an idea of how much I can gain in epressivity using the new language compared to implementations in older languages.

Securities trading is a domain on which I have been working since the last 10 years. I have implemented domain models of securities trading back office systems in Java and Scala. It's time to add scalaz to the mix and see how much more functional my model turns out to be. I have created a playground for this - tryscalaz is a repository on my github that hosts some of my experiments with scalaz. I have started building a domain model for trading systems. It's far from being a realistic one for production use - its main purpose is to make myself more conversant with scalaz.

Scalaz is a wonderful experiment - it's definitely what functional Scala programs should look like. It has a small but wonderful community - Jason (@retronym) and Runar (@runarorama) always help me proactively both on the mailing list and on Twitter.

I am not going into every detail of how my trade domain model shapes up with Scalaz. I implemented a similar domain model in Haskell very recently and documented it here, here and here on my blog. If nothing else, it will help you compare the various aspects of both the implementations.

In this post let me go through some of the features of Scalaz that I found wonderfully expressive to model your domain constraints. You can get a lot out of using Scala only. But with Scalaz, you can take your composition at a much higher level through the various combinators that it offers as part of implementing typeclasses for functors, applicatives, monads and arrows. I haven't yet explored all of these abstractions - yet many of those are already very useful in making your domain models concise, yet expressive.

Here's some example of composition using the higher order functions that Scalaz offers .

 

scala> import scalaz._
import scalaz._

scala> import Scalaz._
import Scalaz._

scala> import net.debasishg.domain.trade.Trades._
import net.debasishg.domain.trade.Trades._

// a Map for trade attributes
scala> val t1 = Map("account" -> "a-123",
"instrument" -> "google", "refNo" -> "r-123", "market" -> "HongKong",
"unitPrice" -> "12.25", "quantity" -> "200")
t1: scala.collection.immutable.Map[java.lang.String,java.lang.String] =
Map((quantity,200), (market,HongKong), (refNo,r-123), (account,a-123), (unitPrice,12.25), (instrument,google))

// get a Trade out of it
scala> val trd1 = makeTrade(t1)
trd1: Option[net.debasishg.domain.trade.Trades.Trade] =
Some(Trade(a-123,google,r-123,HongKong,12.25,200))

// map .. Scala style
scala> (((trd1 map forTrade) map taxFees) map enrichWith) map netAmount
res0: Option[scala.math.BigDecimal] = Some(3307.5000)

Note how we can compose the functions much like the Haskell way that I described in the earlier posts. In the above composition, I used map, which we can do in Scala for lists or options which explicitly support a map operation that maps a function over the collection. With scalaz we can use mapping of a function over any A of kind *->* for which there exists a Functor[A]. Scala supports higher kinds and scalaz uses it to make map available more generally than what you get in the Scala standard library.

Now let's infuse some more Scalaz magic into it. Frequently we need to do the same operations on a list of trades, which means that instead of just a map, we need to lift the functions through another level of indirection. Much like ..
// another trade
scala> val t2 = Map("account" -> "b-123", "instrument" -> "ibm", "refNo" -> "r-234", "market" -> "Singapore", "unitPrice" -> "15.25", "quantity" -> "400")
t2: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map((quantity,400), (market,Singapore), (refNo,r-234), (account,b-123), (unitPrice,15.25), (instrument,ibm))

scala> val trd2 = makeTrade(t2)
trd2: Option[net.debasishg.domain.trade.Trades.Trade] = Some(Trade(b-123,ibm,r-234,Singapore,15.25,400))

scala> ((((List(trd1, trd2)) ∘∘ forTrade) ∘∘ taxFees) ∘∘ enrichWith) ∘∘ netAmount
res1: List[Option[scala.math.BigDecimal]] = List(Some(3307.5000), Some(8845.0000))

Note how the functions forTrade, taxFees etc. get lifted into the List of Options.

Another nice feature that becomes extremely useful with scalaz in a domain model is the use of non-breaking error handling. This is made elegant by designing the Validation[] abstraction as an applicative functor. You can design your validation functions of the domain model as returning an instance of Validation[]. They can then be wired together in a variety of ways to implement accumulation of all failures before reporting to the user .. Here's a simple example from the Trade domain model ..

// validate trade quantity
def validQuantity(qty: BigDecimal): Validation[String, BigDecimal] =
try {
if (qty <= 0) "qty must be > 0".fail
else if (qty > 500) "qty must be <= 500".fail
else qty.success
} catch {
case e => e.toString.fail
}

// validate unit price
def validUnitPrice(price: BigDecimal): Validation[String, BigDecimal] =
try {
if (price <= 0) "price must be > 0".fail
else if (price > 100) "price must be <= 100".fail
else price.success
} catch {
case e => e.toString.fail
}

// make a trade or report validation failures
def makeTrade(account: Account, instrument: Instrument, refNo: String, market: Market,
unitPrice: BigDecimal, quantity: BigDecimal) =
(validUnitPrice(unitPrice).liftFailNel |@|
validQuantity(quantity).liftFailNel) { (u, q) => Trade(account, instrument, refNo, market, u, q) }


Validation[] in scalaz works much like Either[], but has a more expressive interface that specifies the success and error types explicitly ..
sealed trait Validation[+E, +A] {
//..
}

final case class Success[E, A](a: A) extends Validation[E, A]
final case class Failure[E, A](e: E) extends Validation[E, A]

You can use Validation[] in comprehensions or as an applicative functor and wire up your domain validation logic in a completely functional way. Here's how our above validations work on the REPL ..

// failure case
scala> makeTrade("a-123", "google", "ref-12", Singapore, -10, 600)
res2: scalaz.Validation[scalaz.NonEmptyList[String],
net.debasishg.domain.trade.Trades.Trade] =
Failure(NonEmptyList(price must be > 0, qty must be <= 500))

// success case
scala> makeTrade("a-123", "google", "ref-12", Singapore, 10, 200)
res3: scalaz.Validation[scalaz.NonEmptyList[String],
net.debasishg.domain.trade.Trades.Trade] =
Success(Trade(a-123,google,ref-12,Singapore,10,200))



When we have invalid trade arguments all validation errors are accumulated and then reported to the user. If all arguments are valid, then we have a valid Trade instance as success. Cool stuff .. a recipe that I would like to have as part of my domain modeling everytime I start a new project ..

 

From http://debasishg.blogspot.com/2010/12/composable-domain-models-using-scalaz.html

Published at DZone with permission of Debasish Ghosh, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Tags:

Comments

Andrew McVeigh replied on Fri, 2010/12/03 - 6:02am

i really like the idea, and am keen to look at the snippets, but they have html encoding.

e.g.

scala&gt; val t2 = Map("account" -&gt; "b-123", "instrument" -&gt; "ibm", "refNo" -&gt; "r-234", "market" -&gt; "Singapore", "unitPrice" -&gt; "15.25", "quantity" -&gt; "400")

cheers,

andrew

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.