Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala - avoid too complex nested pattern matching

I try to simplify validation process for serving responses for HTTP requests in Spray (I use Slick for database access). Currently, I check in one query if I should go further to the following query or not (return error). This end up with nested pattern matching. Every validation case can return different error so I can not use any flatMap.

class LocationDao {
  val db = DbProvider.db

  // Database tables
  val devices = Devices.devices
  val locations = Locations.locations
  val programs = Programs.programs
  val accessTokens = AccessTokens.accessTokens

def loginDevice(deviceSerialNumber: String, login: String, password: String): Either[Error, LocationResponse] = {
    try {
      db withSession { implicit session =>
        val deviceRowOption = devices.filter(d => d.serialNumber === deviceSerialNumber).map(d => (d.id, d.currentLocationId.?, d.serialNumber.?)).firstOption
        deviceRowOption match {
          case Some(deviceRow) => {
            val locationRowOption = locations.filter(l => l.id === deviceRow._2.getOrElse(0L) && l.login === login && l.password === password).firstOption
            locationRowOption match {
              case Some(locationRow) => {
                val programRowOption = programs.filter(p => p.id === locationRow.programId).firstOption
                programRowOption match {
                  case Some(programRow) => {
                    val program = Program(programRow.name, programRow.logo, programRow.moneyLevel, programRow.pointsForLevel,
                      programRow.description, programRow.rules, programRow.dailyCustomerScansLimit)
                    val locationData = LocationData(program)
                    val locationResponse = LocationResponse("access_token", System.currentTimeMillis(), locationData)
                    Right(locationResponse)
                  }
                  case None => Left(ProgramNotExistError)
                }
              }
              case None => Left(IncorrectLoginOrPasswordError)
            }
          }
          case None => Left(DeviceNotExistError)
        }
      }
    } catch {
      case ex: SQLException =>
        Left(DatabaseError)
    }
  }
}

What is a good way to simplify this? Maybe there is other approach..

like image 369
piobab Avatar asked Feb 25 '15 12:02

piobab


2 Answers

Generally, you can use a for-comprehension to chain together a lot of the monadic structures you have here (including Try, Option, and Either) without nesting. For example:

for {
  val1 <- Try("123".toInt)
} yield for {
  val2 <- Some(val1).map(_ * 2)
  val3 = Some(val2 - 55)
  val4 <- val3
} yield val4 * 2

In your style, this might otherwise have looked like:

Try("123".toInt) match {
    case Success(val1) => {
        val val2 = Some(val1).map(_ * 2)
        val2 match {
            case Some(val2value) => {
                val val3 = Some(val2value - 55)
                val3 match {
                    case Some(val4) => Some(val4)
                    case None => None
                }
            }
            case None => None
        }
    case f:Failure => None
    }
}
like image 100
Ben Reich Avatar answered Nov 19 '22 13:11

Ben Reich


You can define a helper method for Eiterising your control flow.

The benefit of doing this is that you will have great control and flexibility in your flow.

def eitherMe[ I, T ]( eitherIn: Either[ Error, Option[ I ] ],
                      err: () => Error,
                      block: ( I ) => Either[ Error, Option[ T ] ]
                    ): Either[ Error, Option[ T ] ] = {
  eitherIn match {
    case Right( oi ) => oi match {
      case Some( i ) => block( i )
      case None => Left( err() )
    }
    case Left( e ) => Left( e )
  }

}

def loginDevice(deviceSerialNumber: String, login: String, password: String): Either[Error, LocationResponse] = {
  try {
    db withSession { implicit session =>
      val deviceRowOption = devices.filter(d => d.serialNumber === deviceSerialNumber).map(d => (d.id, d.currentLocationId.?, d.serialNumber.?)).firstOption

      val locationRowEither = eitherMe(
        Right( deviceRowOption ),
        () => { DeviceNotExistError },
        deviceRow => {
          val locationRowOption = locations.filter(l => l.id === deviceRow._2.getOrElse(0L) && l.login === login && l.password === password).firstOption
          Right( locationRowOption )
        }
      )

      val programRowEither = eitherMe(
        locationRowEither,
        () => { IncorrectLoginOrPasswordError },
        locationRow => {
          val programRowOption = programs.filter(p => p.id === locationRow.programId).firstOption
          Right( programRowOption )
        }
      )

      val locationResponseEither = eitherMe(
        programRowEither,
        () => { ProgramNotExistError },
        programRow => {
          val program = Program(programRow.name, programRow.logo, programRow.moneyLevel, programRow.pointsForLevel,
            programRow.description, programRow.rules, programRow.dailyCustomerScansLimit)
          val locationData = LocationData(program)
          val locationResponse = LocationResponse("access_token", System.currentTimeMillis(), locationData)
          Right(locationResponse)
        }
      )

      locationResponseEither

    }
  } catch {
    case ex: SQLException =>
      Left(DatabaseError)
  }
}
like image 41
sarveshseri Avatar answered Nov 19 '22 13:11

sarveshseri