3
votes

Suppose I define errors for my Scala application. I want them to be Error, Warning, and Ok. I would like the Error and Warning to contain a human-readable text message.

I would like also to assign numerical codes (0 - for Ok, 1 - for Warning, and 2 - for Error) to find the most serious error in an errors list for example.

So, I define this error stuff as follows:

object MyErrors {  
  abstract sealed case class MyError(code: Int, maybeMessage: Option[String])
  object Ok extends MyError(0, None)
  final case class Warning(message) extends MyError(1, Some(message)) 
  final case class Error(message) extends MyError(2, Some(message))
}

Does it make sense? How would you implement it?

3
Is (2 * Warning == Error) ? :) If not, the concept of arithmetic data types is a bit overspecific. Maybe a rough grouping like in logging (severe, critical, fatal, ...) is better, and can just be modeled by inheritance. Also: The class Either might be useful for you.user unknown
Afterwards, it might be useful if he has more error values... For example, to filter every errors with a code greater than x.Nicolas
Look at how Scala's Parsers Combinators encode results, which have a similar hierarchy.Daniel C. Sobral
@Nicolas It would be better to drop the numeric code and rather use Ordering/Ordered.paradigmatic
@paradigmatic I fully agree. I just have to figure out how to add Ordering.Michael

3 Answers

8
votes

Several issues according to me:

  1. You cannot extend a case class. Consider sealed trait or a simple abstract class
  2. Why are all of them enclosed in the MyErrors object?
  3. Do you really require the Int field? You can use pattern matching for filtering.
  4. Having Ok as an instance of MyError looks semantically wrong. I would rather used Status
  5. Matter of choice, but I Would have defined a subtrait for ErroneousStatus
  6. Have you consider Either or Validation as an alternative before you came with your design ?
2
votes

This does what I think you're trying to do:

object MyErrors extends Enumeration {
  val Ok = Value("ok", 0)
  val Warning = Value("warning", 1)
  val Error = Value("error", 2)

  class TypeVal(val name: String, val code: Int) extends Val(nextId, name)

  protected final def Value(name: String, code: Int) = new TypeVal(name, code)

  sealed case class MyError(error: TypeVal, maybeMessage: Option[String])

  def ok(msg: Option[String] = None) = new MyError(Ok, msg)
  def error(msg: Option[String] = None) = new MyError(Error, msg)
  def warning(msg: Option[String] = None) = new MyError(Warning, msg)

}

Use as:

val e = MyErrors.error() 
// or
val f = MyErrors.ok(Option("Don't worry, be happy"))
f.error.code // Int = 0
f.maybeMessage // Option[String] = Some(Don't worry, be happy)

Obviously, it could be cleaned up quite a bit. But it should get you going.

2
votes

Evolving on the suggestion about renaming your type to Status by Nicolas, here's some code with other niceties:

sealed trait Status {
  def code: Int
}
object Status {
  case object Ok extends Status {
    val code = 0
  }
  sealed trait WithMessage extends Status {
    def message: String
  }
  case class Warning (message: String) extends WithMessage {
    val code = 1
  }
  case class Error (message: String) extends WithMessage {
    val code = 2
  }
}

Then you can use it like so:

scala> Status.Ok
res0: Status.Ok.type = Ok

scala> Status.Warning("blabla")
res1: Status.Warning = Warning(blabla)

scala> Status.Error("blabla").code
res2: Int = 2