I've been tasked with attaching an audit trail onto a bunch of calcuations for reconstruction of values after the fact (i.e. people with business domain knowledge to decipher what went wrong.) The current code looks something like this:
def doSomething = f(x) orElse g(x,y,z) orElse h(p,q,r) orElse default
Each of these returns an Option. The new code should return a tuple of (Option, Audit.)
I've implemented it as
def doSomething = f(x) match{
case None => g_prime(x,y,z)
case x @ Some(_) => (x, SomeAuditObject)
}
//and taking some liberties with the actual signature...
def g_prime(x,y,z) = g(x,y,z) match{
and so on until the "default." Each function chains to the next and the next and so on. I don't like it. It feels way too imperative. I'm missing something. There's some way of thinking about this problem that I'm just not seeing. Other than wrapping the return values into another Option, what is it?
You can use Monads to compose transformations that leave an audit trail. You can compose the audits inside the Monad. Have a look at this answer for further details.
I tried to produce an example for you. I did not know how to handle the final step of the for-comprehension which is a map and provides no audit trail. If you disallow the use of map you cannot use for-comprehensions but have to use plain calls to flatMap.
case class WithAudit[A](value: A, audit: String){
def flatMap[B](f: A => WithAudit[B]): WithAudit[B] = {
val bWithAudit = f(value)
WithAudit(bWithAudit.value, audit + ":" + bWithAudit.audit)
}
def map[B](f: A => B): WithAudit[B] = {
WithAudit(f(value), audit + ":applied unknown function")
}
}
def doSomething(in: Option[Int]): WithAudit[Option[Int]] = WithAudit(
in.map(x => x - 23),
"substract 23"
)
def somethingElse(in: Int): WithAudit[String] = WithAudit(
in.toString,
"convert to String"
)
val processed = for(
v <- WithAudit(Some(42), "input Some(42)");
proc <- doSomething(v);
intVal <- WithAudit(proc.getOrElse(0), "if invalid, insert default 0");
asString <- somethingElse(intVal)
) yield asString
println(processed)
The output will be
WithAudit(
19,
input Some(42)
:substract 23
:if invalid, insert default 0
:convert to String
:applied unknown function
)
Using flatMap to process the value enforces the provision of an audit. If you don't provide map and limit how you can extract the value from the monad (maybe write a log output if you do so) you can be pretty safely assume that every transformation on the value will get logged. And when the value is obtained, you'll get an entry in your log.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With