Home > front end >  Scala update object fields incrementally
Scala update object fields incrementally

Time:05-26

I need to build records with multiple fields.

The record definition is like:

class Record(
  f1: Int,
  f2: String,
  ...
  err: String
)

The logic is like:

record.f1 = get_f1()
record.f2 = get_f2()
...

The calls to get_f1 etc may throw exceptions. When this happen, we set record.err to the exception message.

Right now, my hacky solution is to use a mutable Record instance and patch the fields incrementally. When exception happens, I'll catch it from an outer function and set the err field.

The question is how can I implement this in a more functional way?

CodePudding user response:

This is a (mostly) functional way of doing this:

import scala.util.{Try, Success, Failure}

Try {
  Record(getf1(), getf2(), "")
} match {
  case Success(r) => r
  case Failure(e) => Record(0, "", e.getMessage)
}

If the code inside the Try runs OK then it will return Success and the match will extract the value and return it.

If the code throws an exception then it will be caught by the Try and it will return Failure. The match will catch the error code and then create a value of Return with err set to the error message from the exception.


As others have mentioned, the Either type is a better way of handling the error case as it removes the error information from the Record class itself:

Try {
  Record(getf1(), getf2())
}.toEither

The result will be Right(<record>) on success or Left(<exception>) on error. You can use match to determine which answer was given.

CodePudding user response:

The question is how can I implement this in a more functional way?

By properly modeling this in a functional style.

First, let's remove the err field from the case class, and rather use Either
Second, let's properly handle the nulls using Option
Finally, let's use a functional library like cats.

The end result would be something like this:

import cats.data.EitherNec
import cats.syntax.all._

type Error = String // You may use a better data type.

final case class Record(
  f1: Int,
  f2: String,
  f3: Boolean
)

object Record {
  def fromRawData(f1: Option[Int], f2: Option[String], f3: Option[Boolean]): EitherNec[Error, Record] =
    (
      f1.toRightNec(left = "F1 was empty"),
      f2.toRightNec(left = "F2 was empty"),
      f3.toRightNec(left = "F3 was empty")
    ).parMapN(Record.apply)
}

The EitherNec[E, A] is just an alias for Either[NonEmptyChain[E], A]
Meaning that if it is a Left it will hold all the errors; it also implies that there must be at least one error.

Feel free to ask any follow-up questions you may have.


If you don't want to use cats you can replace the parMapN with a for; but this will stop at the first failure.


You can see the code running here.

  • Related