The extent to which our systems do what we think they should do without paying some terrible price.
- Complex
- Brittle
- Expensive
- Incorrect
- Non Deterministic
- Sneaky
- Simple
- Robust
- Inexpensive
- Correct
- Deterministic
How are software systems organised?
src
|-- main
| |-- java
| |-- resources
| |-- scala
| `-- thrift
`-- test
|-- resources
`-- scala
lol
Functions.
This is "new age" project layout the attempts to solve the following problems:
- Interface design is hard
- Separating concerns is hard
- Testing is hard
- Examples are bad
- Exposed implementations are pathologically evil
Problem: Currency conversion is hard, frequently wrong, and errors are expensive.
In the simplest case, a module is a scope that exposes:
- Pure Data structures
- Algorithms
Simple Project Structure
forex
|--- BUILD
|--- package.scala
|--- Exchange.scala
`--- model
|--- BUILD
|--- Currency.scala
|--- CurrencyPair.scala
`--- ExchangeRate.scala
Package objects define scope that is available throughout the module. They can also help define external interfaces.
package object forex {
/**
* A Currency exchange is a function from a pair
* of currencies, to a result rate. How that rate
* is found, or whether it is, is implementation
* dependent.
*
* @tparam F - The resulting context, i.e. Option, Future, etc.
**/
type Exchange[F[_]] =
model.CurrencyPair => F[model.ExchangeRate]
}
Factories are the ONLY way to use or declare implementations.
package forex
object Exchange {
def apply(ds: DS.FutureIface): Exchange[Future] =
new DSExchangeImpl(ds)
private[forex] class DSExchangeImpl(
underlying: RevenueDataservice.FutureIface
) extends Exchange[Future] {
override def apply(
pair: model.CurrencyPair
): Future[model.ExchangeRate] = ???
}
}
I like to park them in something like "model", but this can also be stored in thrift etc.
package model
sealed class Currency private[model](val iso4217Code: String)
object Currency {
case object USD extends Currency("USD")
case object CAD extends Currency("CAD")
...
}
Tendencies that are observed, but not possible to prove
Desirable properties of all implementations
forex-test
|--- BUILD
|--- ExchangeLaws.scala
`--- model
|--- BUILD
`--- Generators.scala
The Unit Law.
package forex
object ExchangeLaws {
import model.Generators._
def unitLaw[F[_]: Comonad](exchange: Exchange[F]): Prop =
Prop.forAll { pair: CurrencyPair =>
val result = Comonad.extract(
exchange(pair.copy(right=pair.left))
)
result == ExchangeRate(1f)
}
The Inverse Law
def inverseLaw[F[_]: Applicative: Comonad](
exchange: Exchange[F]
): Prop =
Prop.forAll { case CurrencyPair(l, r) =>
val forward = exchange(CurrencyPair(l, r))
val reverse = exchange(CurrencyPair(r, l))
val (x, y) = Comonad.extract(
Applicative.join(forward, reverse)
)
x.rate == (1 / y.rate)
}
We need to describe the complete space of objects to test, computers are good at this.
package forex.model
object Generators {
implicit val ArbCurrency = Arbitrary {
Gen.pick(1, Seq[Currency](USD, CAD, ...))
}
implicit val ArbExchangeRate = Arbitrary {
Gen.posNum[Float].map { n => ExchangeRate(n) }
}
implicit val ArbCurrencyPair = Arbitrary {
for {
l <- ArbCurrency.arbitrary
r <- ArbCurrency.arbitrary
} yield CurrencyPair(l, r)
}
}