1

I have an object that had an optional field on it, and on certain point of my code I need to have this field, so I did something like this:

  def updateReport(task: Task): Future[Task] = for {
      taskResponse <- Future {
        task.response.getOrElse(throw NoExpectedFieldsException(s"expected to have Response for taskId: ${task.taskId}"))
      }
      res <- reportService.updateReportWithResponse(taskResponse) map (_ => task)
    } yield res

task looks something like:

Task(taskId: String, ... , response: Option[Response])

is there a more elegant way of doing that?

thought maybe to add function:

  private def extractOpt[T](elem: Option[T]) = {
    elem.getOrElse(throw NoExpectedFieldsException(s"expected to have element"))
  }

but that just moves the ugliness somewhere else...

Task is a case class I created to be able to create differet type of tasks, it has a field called taskType that determain which type of task it is, and all the required elements are on the task as options

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
user804968
  • 161
  • 1
  • 1
  • 14
  • 1
    When `task.response` would be `None`. Since it is modeled to have option here in some cases it happens. – Mateusz Kubuszok Jul 21 '20 at 12:32
  • 2
    The idea of an **Option** is that the field may or may not exists. If at some point you are _"sure"_ it exists, then it means that either you have a model error or that your assumptions are not 100% accurate and some day they will fail. Anyways, what did you expect with an elegant solution? – Luis Miguel Mejía Suárez Jul 21 '20 at 12:53
  • What about flatMap? Then you get your result or a None? – GamingFelix Jul 21 '20 at 13:05
  • @LuisMiguelMejíaSuárez I created it as an option since I wanted the model Task to be used for different type of tasks with field that called taskType, so you could have different type of tasks and all the elements that are possible for the different type of tasks are optiones. I think its pretty common way of doing something like this, i have seen it around before – user804968 Jul 21 '20 at 13:22
  • @user804968 seems like a pretty common way of doing things in **Java**. What you probably should do is make `Task` a `sealed trait` and create different case classes for each kind of task, where each only has the valid fields for that task. Where you need to be generic you can use pattern matching or define only common fields in the trait, and when you need a specific kind of task you ask precisely for that one, that way you do not have to _unwrap_ fields that will never be none. – Luis Miguel Mejía Suárez Jul 21 '20 at 13:33
  • @LuisMiguelMejíaSuárez even if i created a specific task, a task that have response is something that is created and being sent to another server, and he sends the response and then I populate the task with the response. so the response again will have to be an option untill i get back an answer from the server and update the response – user804968 Jul 21 '20 at 13:35
  • @user804968 you use a different class for making the request and modeling the response. The one returned after the response has that field as a non-optional field. – Luis Miguel Mejía Suárez Jul 21 '20 at 13:40

1 Answers1

2

Option forms a monad so we can map over it

def updateReport(task: Task): Future[Task] = {
  task.response.map { taskResponse =>
    reportService.updateReportWithResponse(taskResponse).map(_ => task)
  }.getOrElse(Future.failed(new NoExpectedFieldsException(s"expected to have Response for taskId: ${task.taskId}")))
}

Option is an algebraic data type so we can pattern match on it

def updateReport(task: Task): Future[Task] = {
  task.response match {
    case None => 
      Future.failed(new NoExpectedFieldsException(s"expected to have Response for taskId: ${task.taskId}"))
      
    case Some(taskResponse) => 
      reportService.updateReportWithResponse(taskResponse).map(_ => task)
  }
}

Option can also be folded to a pure value, as Luis suggests,

def updateReport(task: Task): Future[Task] = {
  task.response.fold(
    Future.failed[Task](new NoExpectedFieldsException(s"expected to have Response for taskId: ${task.taskId}"))
  ) { taskResponse =>
    reportService.updateReportWithResponse(taskResponse).map(_ => task)
  }
}

Option can represent a non-existent value, and for that purpose extracting pure value out of it is an unsafe operation so it is probably a good idea to have unsafe operations appear uglier in code to discourage or make visible their use

task.response.getOrElse(throw NoExpectedFieldsException(s"expected to have Response for taskId: ${task.taskId}"))
Mario Galic
  • 47,285
  • 6
  • 56
  • 98
  • 3
    `map` + `getOrElse` = `fold` – Luis Miguel Mejía Suárez Jul 21 '20 at 13:47
  • @LuisMiguelMejíaSuárez Is there a general name for a structure that provides `fold`? – Mario Galic Jul 21 '20 at 13:55
  • 1
    You mean like `Functor`? There is probably one, but I do not know which one. However, `folds` - However, `folding` means take an **ADT** and one function for each case of the **ADT** and return a plain value. Take a look to [this](https://www.youtube.com/watch?v=cS4MOUsuWYc). - I know the more concrete `foldLeft` and `foldRight` variants for collection-like effects are modeled by `Foldable`. – Luis Miguel Mejía Suárez Jul 21 '20 at 14:01
  • @LuisMiguelMejíaSuárez thanks for the additional suggestion – user804968 Jul 21 '20 at 16:23
  • @MarioGalic why in the first example you dont need to specify the case class of the ```Future.faild``` but in the fold case you have to ```Future.failed[Task]```? – user804968 Jul 21 '20 at 17:17
  • @user804968 Because of the way type inference works in Scala 2 for multiple parameter lists in signatures such as `Option#fold`. Because `Future.failed(new NoExpectedFieldsException(...)` infers to `Future[Nothing]` in first parameter lists, this constraint is enforced in second parameter lists as well. Please see https://stackoverflow.com/a/63021464/5205022 – Mario Galic Jul 21 '20 at 19:12