Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to functionally handle a logging side effect

I want to log in the event that a record doesn't have an adjoining record. Is there a purely functional way to do this? One that separates the side effect from the data transformation?

Here's an example of what I need to do:

val records: Seq[Record] = Seq(record1, record2, ...)

val accountsMap: Map[Long, Account] = Map(record1.id -> account1, ...)

def withAccount(accountsMap: Map[Long, Account])(r: Record): (Record, Option[Account]) = {
  (r, accountsMap.get(r.id))
}

def handleNoAccounts(tuple: (Record, Option[Account]) = {
  val (r, a) = tuple
  if (a.isEmpty) logger.error(s"no account for ${record.id}")
  tuple
}

def toRichAccount(tuple: (Record, Option[Account]) = {
  val (r, a) = tuple
  a.map(acct => RichAccount(r, acct))
}

records
.map(withAccount(accountsMap))
.map(handleNoAccounts) // if no account is found, log
.flatMap(toRichAccount)

So there are multiple issues with this approach that I think make it less than optimal.

The tuple return type is clumsy. I have to destructure the tuple in both of the latter two functions.

The logging function has to handle the logging and then return the tuple with no changes. It feels weird that this is passed to .map even though no transformation is taking place -- maybe there is a better way to get this side effect.

Is there a functional way to clean this up?

like image 309
Adam Richard Avatar asked Oct 16 '25 17:10

Adam Richard


2 Answers

If you're using scala 2.13 or newer you could use tapEach, which takes function A => Unit to apply side effect on every element of function and then passes collection unchanged:

//you no longer need to return tuple in side-effecting function
def handleNoAccounts(tuple: (Record, Option[Account]): Unit = {
  val (r, a) = tuple
  if (a.isEmpty) logger.error(s"no account for ${record.id}")
}

records
.map(withAccount(accountsMap))
.tapEach(handleNoAccounts) // if no account is found, log
.flatMap(toRichAccount)

In case you're using older Scala, you could provide extension method (updated according to Levi's Ramsey suggestion):

implicit class SeqOps[A](s: Seq[A]) {
  def tapEach(f: A => Unit): Seq[A] = {
      s.foreach(f)
      s
  }
}
like image 79
Krzysztof Atłasik Avatar answered Oct 18 '25 05:10

Krzysztof Atłasik


I could be wrong (I often am) but I think this does everything that's required.

records
  .flatMap(r => 
    accountsMap.get(r.id).fold{
      logger.error(s"no account for ${r.id}")
      Option.empty[RichAccount]
    }{a => Some(RichAccount(r,a))})
like image 28
jwvh Avatar answered Oct 18 '25 06:10

jwvh



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!