1

I have an iterable of arrays that I am trying to turn into case classes, and I'm mapping over them to do so. In the event of an array being non-convertable to a case class, I want to log a warning and proceed with the mapping. However, when I implement the warning, the return type changes from Iterable[MyCaseClass] to Iterable[Any] which is not what I want. E.g.:

case class MyCaseClass(s1: String, s2: String)
object MyCaseClass {
  def apply(sa: Array[String]) = new MyCaseClass(sa(0), sa(1))
}

val arrayIterable: Iterable[Array[String]] = Iterable(Array("a", "b"), Array("a", "b", "c"))

def badReturnType(): Iterable[Any] = { // Iterable[Any] is undesireable
  arrayIterable map {
    case sa: Array[String] if sa.length == 2 => MyCaseClass(sa)
    case _ => println("something bad happened!") // but warnings are good
  }
}

def desiredReturnType(): Iterable[MyCaseClass] = { // Iterable[MyCaseClass] is desireable
  arrayIterable map {
    case sa: Array[String] if sa.length == 2 => MyCaseClass(sa)
    // but no warnings if things go wrong!
  }
}

I want to write a function that meets the following criteria:

  1. maps over the Iterable, converting each element to a MyCaseClass
  2. log warnings when I get an array that cant be converted to a MyCaseClass
  3. after logging the warning, the array passed into the match criteria is ignored/discarded
  4. the return type should be Iterable[MyCaseClass].

How can I meet these conditions?

Logister
  • 1,852
  • 23
  • 26

2 Answers2

5

Consider using List instead of Array, and try wrapping in Option in combination with flatMap

l flatMap {
  case e if e.length == 2 => Some(MyCaseClass(e))
  case e => println(s"$e is wrong length"); None
}

Another approach is partitionMap

val (lefts, rights) = l.partitionMap {
  case e if e.size == 2 => Right(MyCaseClass(e))
  case e  => Left(s"$e is wrong length")
}

lefts.foreach(println)
rights
Mario Galic
  • 47,285
  • 6
  • 56
  • 98
3

You can do something like this:

final case class MyCaseClass(s1: String, s2: String)

def parse(input: Array[String]): Either[String, MyCaseClass] = input match  {
  case Array(s1, s2) => Right(MyCaseClass(s1, s2))
  case _ => Left(s"Bad input: ${input.mkString("[", ", ", "]")}")
}

def logErrors(validated: Either[String, _]): Unit = validated match {
  case Left(error) => println(error)
  case Right(_)    => ()
}

def validateData(data: IterableOnce[Array[String]]): List[MyCaseClass] =
  data
    .iterator
    .map(parse)
    .tapEach(logErrors)
    .collect {
      case Right(value) => value
    }.toList

Which you can use like this:

val arrayIterable = Iterable(Array("a", "b"), Array("a", "b", "c"))
validateData(arrayIterable)
// Bad input: [a, b, c]
// res14: List[MyCaseClass] = List(MyCaseClass("a", "b"))
  • 2
    IMHO `tapEach` makes it a draw, so I vote for both answers, but someone should note the advantage of avoiding intermediate collections. – som-snytt Mar 03 '20 at 17:58
  • 1
    @som-snytt Sometimes we even race :) https://stackoverflow.com/a/59903201/5205022 – Mario Galic Mar 03 '20 at 18:06