Статьи

Составные доменные модели с использованием Scalaz

Мне было очень весело работать со скалярным языком — возможно, вы настолько близки к Haskell с постфункциональным языком, таким как Scala, который также поддерживает объектно-ориентированные парадигмы. Один из способов изучения языков — это разработка моделей предметной области с использованием идиом, предлагаемых языком, и попытка сделать модель максимально выразительной. Я выбираю домены, над которыми я работал раньше, — поэтому у меня есть представление о том, сколько я могу получить в экспрессивности, используя новый язык, по сравнению с реализациями на старых языках.

Торговля ценными бумагами — это домен, над которым я работаю последние 10 лет. Я реализовал доменные модели систем бэк-офиса торговли ценными бумагами на Java и Scala. Пришло время добавить в смесь скаляр и посмотреть, насколько более функциональной окажется моя модель. Я создал для этого игровую площадку — tryscalaz — это репозиторий на моем github, в котором проводятся некоторые мои эксперименты со scalaz. Я начал строить модель предметной области для торговых систем. Он далек от того, чтобы быть реалистичным для производственного использования — его главная цель состоит в том, чтобы сделать меня более знакомым со скалазом.

Scalaz — это замечательный эксперимент — он определенно должен выглядеть как функциональные программы Scala. У него есть небольшое, но замечательное сообщество — Джейсон (@retronym) и Рунар (@runarorama) всегда активно помогают мне как в списке рассылки, так и в Твиттере.

Я не буду вдаваться в подробности того, как моя модель предметной области складывается из Scalaz. Я недавно внедрил подобную модель предметной области в Haskell и задокументировал ее здесь , здесь и здесь, в моем блоге. Если ничего другого, это поможет вам сравнить различные аспекты обеих реализаций.

В этом посте я расскажу о некоторых возможностях Scalaz, которые я нахожу удивительно выразительными для моделирования ограничений вашего домена. Вы можете получить много от использования только Scala. Но с помощью Scalaz вы можете поднять свою композицию на гораздо более высокий уровень с помощью различных комбинаторов, которые она предлагает как часть реализации классов типов для функторов, аппликативов, монад и стрелок. Я еще не изучил все эти абстракции — но многие из них уже очень полезны для того, чтобы сделать ваши доменные модели краткими, но выразительными.

Вот несколько примеров компоновки с использованием функций высшего порядка, которые предлагает Scalaz.

 

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)

Обратите внимание, как мы можем составлять функции так же, как способ Haskell, который я описывал в предыдущих постах. В приведенной выше композиции я использовал map, что мы можем сделать в Scala для списков или опций, которые явно поддерживают операцию map, которая отображает функцию над коллекцией. С помощью скалаза мы можем использовать отображение функции на любой тип A * -> *, для которого существует функтор [A]. Scala поддерживает
более высокие виды и scalaz использует его , чтобы сделать карту доступны более
обычно , чем то , что вы получите в стандартной библиотеке Scala.

Теперь давайте добавим еще немного магии Скалаза. Часто нам нужно выполнять одни и те же операции со списком сделок, что означает, что вместо простого отображения карты нам необходимо поднять функции через другой уровень косвенности. Так же, как ..

// 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))

Обратите внимание, как функции forTrade, taxFees и т. Д. Попадают в Список опций.

Еще одна приятная особенность, которая становится чрезвычайно полезной при использовании скалаза в доменной модели, — это использование непрерывной обработки ошибок. Это сделано изящно, проектируя абстракцию Validation [] как аппликативный функтор. Вы можете спроектировать свои функции проверки модели домена как возвращающие экземпляр Validation []. Затем их можно соединить различными способами, чтобы реализовать накопление всех сбоев, прежде чем сообщать об этом пользователю. Вот простой пример из модели области торговли.

// 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) }

Валидация [] в scalaz работает во многом как
Either [] , но имеет более выразительный интерфейс, который явно указывает типы успеха и ошибок.

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]

Вы можете использовать Validation [] в пониманиях или в качестве аппликативного функтора и полностью подключить логику проверки домена. Вот как работают наши проверки на 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))

Когда у нас есть недействительные торговые аргументы, все ошибки валидации накапливаются и затем сообщаются пользователю. Если все аргументы верны, то мы имеем действительный экземпляр Trade как успешный. Классная штука .. рецепт, который я хотел бы иметь при моделировании моего домена каждый раз, когда начинаю новый проект ..

 

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