Scala - Avoid overly complex nested pattern matching

I am trying to simplify the verification process to respond to HTTP requests in Spray (I use Slick to access the database). I am currently checking one query if I have to go to the next query or not (return error). The result is a nested pattern matching. Each test case may return different errors, so I cannot 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? Perhaps there is another approach.

+5
source share
3 answers

Typically, you can use the many monadic structures you have here to understand (including Try , Option and Either ) without nesting. For instance:

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

In your style, it might look different:

 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 } } 
+6
source

You can define a helper method for Eiter that will use your control flow.

The advantage of this is that you will have excellent 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) } } 
+1
source

For me, when I someday cannot escape complex nesting, I would extract a piece of code that makes sense together and turns it into a new method and gives it a meaningful name. This will document the code and make it more readable and reduce complexity within each individual method. And usually, as soon as I do this, I can see the stream better and could reorganize it to make more sense (after writing the tests the first time to cover the behavior I want).

eg. for your code, you can do something like this:

 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 => checkDeviceRowOption(deviceSerialNumber, login, password) } } catch { case ex: SQLException => Left(DatabaseError) } } def checkDeviceRowOption(deviceSerialNumber: String, login: String, password: String): Either[Error, LocationResponse] = { 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) => { checkProgramRowOption(locationRow) } case None => Left(IncorrectLoginOrPasswordError) } } case None => Left(DeviceNotExistError) } } def checkProgramRowOption(locationRow: LocationRowType): Either[Error, LocationResponse] = { 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) } } } 

Please note that this is just an illustration and probably will not compile because I do not have your library, but you should be able to configure the code to compile it.

0
source

Source: https://habr.com/ru/post/1214163/


All Articles