Best way to merge two case classes

Hi,

I’m pretty new to Scala (although not to programming in general) so am looking for someone wiser/cleverer than myself to give some advice on the best way to achieve merging case classes together. I’m using this in the context of Lagom but I think this is more of a general Scala question rather than Lagom-specific. Please forgive me for any glaring mistakes I’ve made - the learning curve is quite steep here - but please do point them out! Also, please note that using the code provided I am able to achieve what I wanted to achieve - just not in a style that I am comfortable with.

With Lagom it seems to be encouraged to use case classes to pass data around to each part of the eco-system of microservices that one is assembling.

Basically, I have two microservices: survey and question - each one handles persisting their own data separately - questions can be updated independently of surveys (and vice-versa). When a survey is saved, it is saved with only references to questions (via the ref field in PageQuestion which will be used to populate the mutable question field later on).

I have the following case classes in my Survey microservice. Note that in the PageQuestion class I am currently using a var field for questions because I need to be able to amalgamate the PageQuestion’s question field with data from the question service’s Question object.

case class Survey(uuid: String, name: String, pages: Option[Array[Page]])
object Survey {
  implicit val format: Format[Survey] = Json.format
}

case class Page(uuid: String, name: String, pos: Int, questions: Option[Array[PageQuestion]])
object Page {
  implicit val format: Format[Page] = Json.format
}

case class PageQuestion(ref: String, pos: Int, var question: Option[Question])
object PageQuestion {
  implicit val format: Format[PageQuestion] = Json.format
}

And the following in my Question microservice.

case class Question(uuid: String, title: String, description: String)
object Question {
  implicit val format: Format[Question] = Json.format
}

When I come to re-assemble a Survey object I am using the following function that iterates through the survey and gets the Question data from the question service and then appends it onto the question field of the PageQuestion nested object:

override def getSurvey(id: String) = ServiceCall { request =>
    Console.println("getting survey")
    refFor(id).ask(GetSurvey).map {
      case Some(survey) =>
        //below is the function I need help with
        amalgamateSurveyWithQuestions(Survey(survey.uuid, survey.name, survey.pages))
      case None =>
        throw NotFound(s"Survey with id $id")
    }
  }

//this function below just gets the Question from the questionService and returns a Future[Question]
def getSurveyQuestion(id: String) = questionService.getQuestion(id: String).invoke(NotUsed)

//this function is what I have so far, taking advantage of the var question field in PageQuestion
def amalgamateSurveyWithQuestions(survey: Survey): Survey = {
survey.pages.foreach(pages => {
      pages.foreach(page => {
        page.questions.foreach(pagequestions => {
          pagequestions.foreach(pagequestion => {
            getSurveyQuestion(pagequestion.ref).map { question =>
              pagequestion.question = Some(question).orElse(None)
            }
          })
        })
      })
    })
    survey
  }

Based on the snippets provided, can anyone sggest any better way for achieving what I want to do? From what I understand about case classes, using vars / mutable fields is not encouraged and, somehow, I feel that the way that I have written the amalgamateSurveyWithQuestions() function is clunky and rather un-scala-like. Would it be better to iterate through the survey.pages array and rebuild a new Survey object using the copy function that gets added to case classes? The reason why I have done it the way above is purely for convenience as it seemed going through all the nested Pages and PageQuestions and copying them into new arrays was a bit of a hassle!

Can anyone with more experience guide me here? If anything is unclear, please let me know. Any help greatly appreciated!

Tom

That’s how I’d do it. I’d probably replace the Option[Array[]] with List[] = Nil because it’ll be simpler and there is probably no useful distinction between None and Nil.

def amalgamateSurveyWithQuestions(survey: Survey): Survey = {
    survey.copy(pages = pages.map { page =>
        page.pagequestions.map { pagequestion =>

and so on.

I always find it a bit suspicious when you need to build an object structure across microservices using references - makes me wonder whether they really should be separate? I guess it depends where Question serves a purpose on it’s own.

Hope that helps.

I think it’s much more a DDD / Microservices question than a Scala case class question. Consider asking it on the Lagom mailing list https://groups.google.com/forum/?hl=en-GB#!forum/lagom-framework , I’m sure you’ll get some helpful feedback there.

Great, thanks for your advice, Brian. Indeed I am grappling with the question of whether the question (no pun intended) should be part of a larger microservice that handles both survey and question entities - otherwise if one becomes unavailable then surveys will be screwed (I think that’s the technical term for it).

I’ll try over there too… Thanks for your reply.

OK, so my solution so far is this… It’s probably not perfect but it doesn’t involve mutable case class fields (and I have swapped out Arrays with Lists - although it does include a blocking Await call that I would love to get rid of somehow. I’ve also brought the Question entity into the Survey service (a Lagom thing) so that there is no cross-service dependency. However, the thing that concerns me most at the moment is how to work with Futures and case classes - which, I think, is more of a general Scala language thing rather than specifically Lagom.

The case classes:

case class Question(uuid: String, title: String, description: String)
object Question {
  implicit val format: Format[Question] = Json.format
}

case class Survey(uuid: String, name: String, pages: List[Page] = Nil)
object Survey {
  implicit val format: Format[Survey] = Json.format
}

case class Page(uuid: String, name: String, pos: Int, questions: List[PageQuestion] = Nil)
object Page {
  implicit val format: Format[Page] = Json.format
}

case class PageQuestion(ref: String, pos: Int, question: Option[Question])
object PageQuestion {
  implicit val format: Format[PageQuestion] = Json.format
}

The business logic:

def getNewPageQuestionWithQuestion(pagequestion: PageQuestion) = Await.result(refForQuestion(pagequestion.ref).ask(GetQuestion).map {
            case Some(question) =>
              pagequestion.copy(question = Option(Question(question.uuid, question.title, question.description)))
            case None =>
              throw NotFound(s"Question with id $pagequestion.ref")
          }, Duration(100, MILLISECONDS))

  def amalgamateSurveyWithQuestions(survey: Survey): Survey = {  
    survey.copy(pages = survey.pages.map { page =>
      page.copy(questions = page.questions.map { pagequestion =>
        getNewPageQuestionWithQuestion(pagequestion)
      })
    })
  }

Let me know what you think - especially if there’s anything to say about making the Await call and relying on blocking.

Thanks, again!

First, good work on avoiding the var. Especially with case classes you want to construct a new instance for modification rather than mutating.

You can clean up your code slightly by using a pattern alias for the Some(question) match

.map {
  case theQuestion @ Some(question) =>
    pagequestion.copy(question = theQuestion)
  case None => ...

There’s no good way to get rid of the Await.result if you want to return a value you are getting from a Future based call. Normally you would let the Future’s bubble up through the call stack until the last second before the value is needed. Along the way you still get to treat the result as a value (by mapping it) but you don’t have to block until you are absolutely sure you need the result. To facilitate that I recommend keeping the future separate from the Await. I.e.,

import scala.concurrent.duration._

def getNewPageQuestionWithQuestion(pageQuestion: PageQuestion): PageQuestion = {
  val fx = refForQuestion(pagequestion.ref).ask(GetQuestion).map {...}
  Await.result(fx, 100.millis)
}

This let’s you easily refactor to just returning the Future and makes the blocking call easier to see.

Finally I’ll note that your Await.result can throw an exception that I don’t see you handling. (Another good reason to just return a Future[PageQuestion] and handle blocking and error handling higher up the stack).

Excellent reply, Lanny, just the sort of feedback I was looking for. I’ll take note and adjust. Thanks!