2

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?

wheaties
  • 35,646
  • 15
  • 94
  • 131
  • 1
    Have a look at this answer (Hint: the Monoid is your audit): http://stackoverflow.com/questions/2322783/how-to-add-tracing-within-a-for-comprehension/4871353#4871353 – ziggystar Sep 16 '11 at 19:07

2 Answers2

8

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
)

Safety

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.

Community
  • 1
  • 1
ziggystar
  • 28,410
  • 9
  • 72
  • 124
  • I'm actually not looking to put it in a log file. It's completely side effect free. I'm looking to return a Tuple but conditionally compose the functions based upon the condition of one of the values of the Tuple. – wheaties Sep 16 '11 at 19:32
  • @wheaties You can use any function you like in the way I described. Do you want an example with tuples using my code? And concerning the logs: I meant that you should only allow some defined "trusted" code (that might leave a log or whatever) to get the value from the `WithAudit` object in case you want some type checking whether you did not forget any audits. – ziggystar Sep 16 '11 at 19:42
  • I see. So, recalling from what little I know of Scalaz you'd say what I need should be something akin to an MAB type? – wheaties Sep 16 '11 at 19:49
  • @wheaties Sorry, I'm not very familiar with scalaz and I don't understand all the $&&§ method names in the source of MAB. But I think you could be right.udits to a complete trail for you using flatMap. And additionally – ziggystar Sep 16 '11 at 19:59
2

Do you only audit successful executions of f, g, etc.?

If it is so, I'd make doSomething return Option[(YourData, Audit)] (instead of (Option[YourData], Audit)). You could then compose the functions like this:

def doSomething = (f(x) andThen (t => (t, audit_f(x)))) orElse
                  (g(x, y, z) andThen (t => (t, audit_g(x, y, z)))) orElse
                  ... etc.
jpalecek
  • 47,058
  • 7
  • 102
  • 144
  • No, we audit everything. And each function has a different audit value. If it were so simple as "pass, fail" I'd probably just use an Either. – wheaties Sep 16 '11 at 18:36
  • 1
    Looking at your code, you are indeed, at jpalecek suggest, auditing only __successful__ executions. That is, when f returns None, you keep no audit from f and go to g directly. Which makes it looks more like Option[(Result, Audit)] than (Option[Result], Audit). Which is not the same as Either[Result, Audit], where you would have an Audit when it fails. – Didier Dupont Sep 16 '11 at 18:59