- Alan J Perlis
When you model a domain you map the artifacts from the problem domain to the solution domain. The problem domain artifacts are the same irrespective of your solution domain. But the mapping process depends on your medium of modeling, the target platform, the programming language and the paradigms that it offers. Accordingly you need to orient your thought process so as to adapt to the language idioms that you have chosen for implementation.
Recently I did a fun exercise in modeling the same problem domain on to 2 target languages - Scala, that offers a mix of OO and functional features and Clojure, that's much more functional and makes you think more in terms of functions and combinators. The idea is to share the thought process of this domain modeling exercise and demonstrate how even similar architectural patterns of the solution can map to entirely different paradigms in the implementation model.
This is also one of the underlying themes of my upcoming book DSLs In Action. When you think of a DSL you need to think not only of the surface syntax that the user gets to know, but also of the underlying domain model that forms the core of the DSL implementation. And the underlying host language paradigms shape the way you think of your DSL implementation. In the book there are plenty of examples where I take similar examples from one problem domain and design DSLs in multiple languages. That way you get to know how your thought process needs to be re-oriented when you change implementation languages even for the same problem at hand.
Modeling in Scala, a hybrid object functional language
Consider an abstraction for a security trade. For simplicity we will consider only a small set of attributes meaningful only for the current discussion. Let's say we are modeling a
Trade
abstraction using a statically typed language like Scala that offers OO as one of the paradigms of modeling. Here's a sample implementation that models a Trade
as a case class in Scala ..Objects for coarse-grained abstractions ..
type Instrument = String
type Account = String
type Quantity = BigDecimal
type Money = BigDecimal
import java.util.{Calendar, Date}
val today = Calendar.getInstance.getTime
case class Trade(ref: String, ins: Instrument, account: Account, unitPrice: Money,
quantity: Quantity, tradeDate: Date = today) {
def principal = unitPrice * quantity
}
Ok .. that was simple. When we have classes as the primary modeling primitive in the language we try to map artifacts to objects. So the trade artifact of the problem domain maps nicely to the above class
Trade
.But a trade has a definite lifecycle and a trade abstraction needs to be enriched with additional attributes and behaviors in course of the various stages of the trading process. How do we add behaviors to a trade dynamically ?
Consider enriching a trade with tax and fee attributes when we make an instance of a
Trade
. Similar to Trade
we also model the tax and fee types as separate artifacts that can be mixed in with the Trade
abstraction.trait TradeTax { this: Trade =>
def tradeTax(logic: Money => Money): Money = logic(principal)
}
trait Commission { this: Trade =>
def commission(logic: (Money, Quantity) => Money): Money = logic(principal, quantity)
}
Now we can instantiate a trade decorated with the additional set of taxes and fees required as per the market regulations ..
lazy val t = new Trade("1", "IBM", "a-123", 12.45, 200) with TradeTax with Commission
Note how the final abstraction composes the
Trade
class along with the mixins defined for tax and fee types. The thought process is OO along with mixin inheritance - a successful implementation of the decorator pattern. In a language that offers classes and objects as the modeling primitives, we tend to think of them as abstracting the coarse grained artifacts of the domain.Functions for fine grained domain logic ..
Also note that we use higher order functions to model the variable part of the tax fee calculation. The user supplies this logic during instantiation which gets passed as a function to the calculation of
TradeTax
and Commission
.lazy val tradeTax = t.tradeTax { p => p * 0.05 }
lazy val commission = t.commission { (p, q) => if (q > 100) p * 0.05 else p * 0.07 }
When modeling with Scala as the language that offers both OO and functional paradigms, we tend to use the combo pack - not a pure way of thinking, but the hybrid model that takes advantage of both the paradigms that the language offers.
And now Clojure - a more functional language than Scala
As an alternate paradigm let's consider the same modeling activity in Clojure, a language that's more functional than Scala despite being hosted on top of the Java language infrastructure. Clojure forces you to think more functionally than Scala or Java (though not as purely as Haskell). Accordingly our solution domain modeling thoughts also need to take a functional bend. The following example is also from my upcoming book DSLs In Action and illustrates the same point while discussing DSL design in Clojure.
Trade
needs to be a concrete abstraction and one way of ensuring that in Clojure is as a Map
. We can also use defrecord
to create a Clojure record, but that's not important for the point of today's discussion. We model a trade as a Map
, but abstract its construction behind a function. Remember we are dealing with a functional language and all manipulations of a trade need to be thought out as pure functions that operate on immutable data structures.This is how we construct a trade from a request, which can be an arbitrary structure. In the following listing,
trade
is the constructor function that returns a Map
populated from a request structure.Abstractions to map naturally to functions ..
; create a trade from a request
(defn trade
"Make a trade from the request"
[request]
{:ref-no (:ref-no request)
:account (:account request)
:instrument (:instrument request)
:principal (* (:unit-price request) (:quantity request))
:tax-fees {}})
0x3b a sample request
(def request
{:ref-no "trd-123"
:account "nomura-123"
:instrument "IBM"
:unit-price 120
:quantity 300})
In our case the
Map
just acts as the holder of data, the center of attraction is the function trade
, which, as we will see shortly, will be the main subject of composition.Note that the
Map
that trade
returns contains an empty tax-fees
structure, which will be filled up when we decorate the trade with the tax fee values. But how do we do that idiomatically, keeping in mind that our modeling language is functional and offers all goodness of immutable and persistent data structures. No, we can't mutate the Map
!Combinators for wiring up abstractions ..
But we can generate another function that takes the current
trade
function and gives us another Map
with the tax-fee values filled up. Clojure has higher order functions and we can make a nice little combinator out of them for the job at hand ..; augment a trade with a tax fee value
(defn with-values [trade tax-fee value]
(fn [request]
(let [trdval (trade request)
principal (:principal trdval)]
(assoc-in trdval [:tax-fees tax-fee]
(* principal (/ value 100))))))
Here
trade
is a function which with-values
takes as input and it generates another function that decorates the original trade Map
with the passed in tax-fee
name and the value. Here the value that we pass is a percentage that's calculated based on the principal of the trade. I have kept it simpler than the Scala version that models some more logic for calculating each individual tax fee value. This is the crux of the decorator pattern of our model. We will soon dress it up a little bit and give it a user friendly syntax.and Macros for DSL ..
Now we can define another combinator that plays around nicely with with-values to model the little language that our users can relate to their trading desk vocabulary.
Weeee .. it's a macro :)
; macro to decorate a function
(defmacro with-tax-fee
"Wrap a function in one or more decorators"
[func & decorators]
`(redef ~func (-> ~func ~@decorators)))
and this is how we mix it with the other combinator with-values ..
(with-tax-fee trade
(with-values :tax 12)
(with-values :commission 23))
Note how we made
with-tax-fee
a decorator that's replete with functional idioms that Clojure offers. ->
is a Thrush combinator in Clojure that redefines the original function trade
by threading it through the decorators. redef
is just a helper macro that redefines the root binding of the function preserving the metadata. This is adapted from the decorator implementation that Compojure offers.We had the same problem domain to model. In the solution domain we adopted the same overall architecture of augmenting behaviors through decorators. But the solution modeling medium were different. Scala offers the hybrid of OO and functional paradigms and we used both of them to implement decorator based behavior in the first case. In the second effort, we exploited the functional nature of Clojure. Our decorators and the subject were all functions and using the power of higher order functions and combinator based approach we were able to thread the subject through a series of decorators to augment the original trade abstraction.