0

I am coming from Java background and learning Scala now. I have a Seq respectively for Loc and LocSize objects whereby these 2 objects share a common member field code. I have a validateRoom function that basically validates these two Seq by matching a Loc instance to a LocSize instance and make some basic validation operations. My objective is to make sure all Loc objects passes the validation, hence I am using Seq.forall at the last println.

I believe that there are better, shorter and prettier FP ways to achieve what I want. How can I do so?

case class Loc(id: String, code: String, isRoom: Boolean, measureUnit: String, allowedSize: Int)

case class LocSize(code: String, width: Int, length: Int, measureUnit: String)

val locs = Seq(
  Loc("_1", "loc01", true, "M", 100),
  Loc("_2", "loc02", false, "M", 100),
  Loc("_3", "loc03", true, "M", 100)
)

val locSizes = Seq(
  LocSize("loc01", 5, 10, "M"),
  LocSize("loc02", 6, 11, "M"),
  LocSize("loc03", 9, 14, "M"),
  LocSize("loc04", 8, 13, "M"),
  LocSize("loc05", 9, 14, "M"),
)

def validateRoom(locs: Seq[Loc], loSizes: Seq[LocSize]): Seq[Boolean] = {
  for (loc <- locs) yield {
    if (loc.isRoom) {
      val locSize = loSizes.find(_.code == loc.code)

      locSize match {
        case Some(i) => {
          if (i.measureUnit.contains(loc.measureUnit) &&
           i.width*i.length < loc.allowedSize)
            true
          else
            false
        }
        case None => false
      }
    }
    else
      true
  }
}

// Return false as loc03 size 9*14 > 100
println(validateRoom(locs, locSizes).forall(b => b))
Tomer Shetah
  • 8,413
  • 7
  • 27
  • 35
Wilts C
  • 1,720
  • 1
  • 21
  • 28

4 Answers4

2

YMMV and there are probably other more elegant solutions, here is one example


def validateRooms(locs: Seq[Loc], loSizes: Seq[LocSize]): Boolean = {
  locs.foldLeft(true){ (state, loc) =>
    if (state) {
      val locSize = loSizes.find(_.code == loc.code)
      locSize.map( s =>
                 s.measureUnit.contains(loc.measureUnit) &&
           s.width*s.length < loc.allowedSize).getOrElse(false)
    } else {
      state
    }
  }
}

In this case the function returns the result true if all are valid eliminating the forall. Uses map and foldleft to eliminate the for comprehension.

Bhaskar
  • 594
  • 2
  • 11
1

A better functional approach is to not really in Booleans at all, but rather provide more meaningful types; for example, an Either.
If you are open to using cats, the code is as simple as:

import cats.syntax.all._

// In general, a validation should return a new type, maybe a LocWithSize or something.
// The idea of a new type is to proof the type system that such value is already validated.
def validateRoom(locs: List[Loc], loSizes: List[LocSize]): Either[String, List[Loc]] ={
  val sizesByCode = locSizes.iterator.map(ls => ls.code -> ls).toMap
  
  locs.traverse { loc =>
    sizesByCode
      .get(key = loc.code)
      .toRight(left = s"The code: '${loc.code}' was not found in the sizes")
      .flatMap { locSize =>
        val size = locSize.width * locSize.length
        
        if (locSize.measureUnit != loc.measureUnit)
          Left(s"The mesaure unit: '${locSize.measureUnit}' was not applicable to the location: ${loc}")
        else if (size > loc.allowedSize)
          Left(s"The size of location '${loc.code}' was ${size} which is bigger than the allowed size (${loc.allowedSize})")
        else
          Right(loc)
      }
  }
}

Which you can use like this:

val locs = List(
  Loc("_1", "loc01", true, "M", 100),
  Loc("_2", "loc02", false, "M", 100),
  Loc("_3", "loc03", true, "M", 100)
)

val locSizes = List(
  LocSize("loc01", 5, 10, "M"),
  LocSize("loc02", 6, 11, "M"),
  LocSize("loc03", 9, 14, "M"),
  LocSize("loc04", 8, 13, "M"),
  LocSize("loc05", 9, 14, "M")
)

validateRoom(locs, locSizes)
// res: Either[String, List[Loc]] = Left(The size of location 'loc03' was 126 which is bigger than the allowed size (100))

If you do not want to pull out cats just for this, you can implement your own traverse.
Take a look to this for an example using Try, the logic should be pretty similar.


You can see the code running here.

1

Another option you have is:

def validateRoom(locs: Seq[Loc], locSizes: Seq[LocSize]): Boolean = {
  locs.forall(loc => !loc.isRoom || locSizes.find(_.code == loc.code).exists(locSize => {
    locSize.measureUnit.contains(loc.measureUnit) &&
      locSize.width*locSize.length < loc.allowedSize
  }))
}

Why does it work?

forall makes sure for any loc in locs, the condition should be true.

What is the codition?

First, loc.isRoom should br true, then locSizes should find LocSize with the same code. The result of find is an Option, which exposes the method exists. exists makes sure the option is not empty, and the boolean inside is true.

Code run at Scastie.

Tomer Shetah
  • 8,413
  • 7
  • 27
  • 35
0

You can go cleaner like:

def validateRoom(locs: Seq[Loc], loSizes: Seq[LocSize]): Boolean = {
    val locSizeIndex: Map[String, LocSize] = loSizes.map(loc => loc.code -> loc).toMap
    val validation = for {
      room <- locs.filter(_.isRoom)
      size <- locSizeIndex.get(room.code)
    } yield size.measureUnit.contains(room.measureUnit) && size.width * size.length < room.allowedSize
    validation.reduceOption(_ && _).getOrElse(true)
}

// Return false as loc03 size 9*14 > 100
println(validateRoom(locs, locSizes))

Which prints out false

Scatie for playing: https://scastie.scala-lang.org/xQbDWZXOR16PJhz6xeaByA

Ivan Kurchenko
  • 4,043
  • 1
  • 11
  • 28