6

I'm attempting to model responses from REST APIs as case classes which I can use pattern matching on.

I thought it would be a good fit assuming inheritance, but I see that this is deprecated. I know there are already questions related to case classes and inheritance, but my question is more about how you would model the following the "right way" here without inheritance.

I started with the following two case classes, which work fine:

case class Body(contentType: String, content: String)
case class Response(statusCode: Int, body: Body)

i.e. a REST call would return with something like:

Response(200, Body("application/json", """{ "foo": "bar" }"""))

which I could pattern match like:

response match {
  case Response(200, Body("application/json", json)) => println(json)
  case Response(200, Body("text/xml", xml)) => println(xml)
  case Response(_,_) => println("Something unexpected")
}

etc. which works fine.

Where I ran into trouble is: I'd like helper extensions for these case classes, such as:

case class OK(body: Body) extends Response(200, body)
case class NotFound() extends Response(404, Body("text/plain", "Not Found"))

case class JSON(json: String) extends Body("application/json", json)
case class XML(xml: String) extends Body("text/xml", xml)

so that I can do simplified pattern matches like this:

response match {
  case OK(JSON(json)) => println(json)
  case OK(XML(xml)) => println(xml)
  case NotFound() => println("Something is not there")

  // And still drop down to this if necessary:
  case Response(302, _) => println("It moved")
}

and also which would also allow my REST code to directly use and return:

Response(code, Body(contentType, content))

which is easier to build a response dynamically.

so...

I can get it to compile (with deprecation warnings) via:

case class OK(override val body: Body) extends Response(200, body)

However, this doesn't seem to work with pattern matching.

Response(200, Body("application/json", "")) match {
  case OK(_) => ":-)"
  case _ => ":-("
}
res0: java.lang.String = :-(

Any ideas on how this could work? I'm open to different approaches, but this was my attempt to find a practical use for case classes

0__
  • 66,707
  • 21
  • 171
  • 266
7zark7
  • 10,015
  • 5
  • 39
  • 54

3 Answers3

10

There are several reasons why case classes shouldn't be subclassed. In your case, the problem becomes that OK is another type than (a subtype of) Response, therefore the match fails (even if the arguments match, the type doesn't match).

You will want custom extractors, instead. For example:

case class Response(code: Int, body: String)
object OK {
  def apply(body: String) = Response(200, body)
  def unapply(m: Response): Option[String] = m match {
    case Response(200, body) => Some(body)
    case _                   => None
  }
}

def test(m: Response): String = m match {
   case OK(_) => ":-)"
   case _     => ":-("
}

test(Response(300, "Hallo"))  // :-(
test(Response(200, "Welt"))   // :-)
test(OK("Welt"))              // :-)

There are few more examples of custom extractors in this thread.

Community
  • 1
  • 1
0__
  • 66,707
  • 21
  • 171
  • 266
  • Ah, thank you - I see I've totally missed the purpose of unapply until this; this is very helpful. Will test this out fully out with my code to make sure I have covered and will accept later tonight. – 7zark7 Jun 25 '12 at 02:28
  • Good answer @Sciss. Custom extractors are one of the things I like a lot about Scala. – mergeconflict Jun 25 '12 at 06:20
  • @7zark7 Note that when you use custom extractors you lose exhaustiveness guarantees of sealed classes. – Daniel C. Sobral Jun 25 '12 at 14:48
  • @DanielC.Sobral yes I'm seeing that - I'd prefer to use strictly case classes here, but this doesn't seam feasible with the usage requirements I outlined above. – 7zark7 Jun 25 '12 at 16:42
1

Have you looked at the scala library unfiltered? http://unfiltered.lessis.me/ It may help you with approaching you problem . HTH

AndreasScheinert
  • 1,918
  • 12
  • 18
  • I had a look but I quit because there were too many slides, with 1 sentence/a-few-words on each. Is there perhaps any single page version that clarifies what Unfiltered is all about? – KajMagnus Aug 16 '12 at 08:45
1

While custom extractors mentioned by 0__ can certainly be used, you'll lose exhaustiveness guarantees of sealed type hierarchies. While in the example you gave in the question there's nothing sealed, the problem is well suited to them.

In that case, my suggestion is to simply make sure the case class is always at the bottom of the type hierarchy, and make the upper classes normal. For example:

sealed class Response(val statusCode: Int, val body: Body) sealed
case class Ok(override val body: Body) extends Response(200, body)
sealed class NotOk(statusCode: Int, body: Body) extends Response(statusCode, body)
case object NotFound extends NotOk(404, "Not found")
// and so on...
0__
  • 66,707
  • 21
  • 171
  • 266
Daniel C. Sobral
  • 295,120
  • 86
  • 501
  • 681
  • Thanks Daniel, while my first impression was this would not work if I also wanted to allow matches on Response - I see this may work if I define unapply on a Response object as Sciss mentions, and have the "helpers" be the case classes. Will try both approaches out today and see what fits/works best here. – 7zark7 Jun 25 '12 at 18:31
  • Did you mean to write `sealed class Response`? – 0__ Jun 25 '12 at 20:07
  • @Sciss Yes, and `NotOk` as well. Thanks for pointing out my mistake. – Daniel C. Sobral Jun 25 '12 at 20:13