Home > Software engineering >  Is it possible to pass dynamic values in default replacement of ZipAll in Scala?
Is it possible to pass dynamic values in default replacement of ZipAll in Scala?

Time:12-03

I was using zipAll for two unequal sized Lists, as shown below:

val prices = List(5, 10, 15, 20)
val fruits = List("Guava", "Banana", "Papaya", "Apple", "Mango")

I am using zipAll as shown below:

fruits zipAll (prices, "Undefined Fruit", "Price Unavailable")

Which yields following result:

List((Guava,5), (Banana,10), (Papaya,15), (Apple,20), (Mango,Price Unavailable))

What I am interested in is passing dynamic default fillers; so in this case, I am expecting something like:

fruits zipAll (prices, "Undefined Fruit", s"Price Unavailable for $fruitName")

Which should yield:

List((Guava,5), (Banana,10), (Papaya,15), (Apple,20), (Mango,Price Unavailable for Mango))

CodePudding user response:

Since the stdlib doesn't provide such functionality, you need to implement it yourself like this:

def zipAllDynamic[A, B](left: List[A], right: List[B])(leftDefault: B => A, rightDefault: A => B): List[(A, B)] =
  left
    .map(l => Option(l))
    .zipAll(right.map(r => Option(r)), None, None)
    .collect {
      case (Some(l), Some(r)) => (l, r)
      case (Some(l), None) => (l, rightDefault(l))
      case (None, Some(r)) => (leftDefault(r), r) 
    }

Although, this is very expensive.
You may increase the efficiency of the operation using a tail-recursive function; or you can just use the magic of lazynes:

def zipAllDynamic[A, B](left: List[A], right: List[B])(leftDefault: B => A, rightDefault: A => B): List[(A, B)] =
  left
    .iterator.map(l => Option(l))
    .zipAll(right.iterator.map(r => Option(r)), None, None)
    .collect {
      case (Some(l), Some(r)) => (l, r)
      case (Some(l), None) => (l, rightDefault(l))
      case (None, Some(r)) => (leftDefault(r), r) 
    }.toList

Then you can use it like this:

val prices = List(5, 10, 15, 20)
val fruits = List("Guava", "Banana", "Papaya", "Apple", "Mango")

val result = zipAllDynamic(fruits, prices.map(_.toString))(_ => "Undefined Fruit", fruitName => s"Price Unavailable for ${fruitName}")
// result: List[(String, String)] = List((Guava,5), (Banana,10), (Papaya,15), (Apple,20), (Mango,Price Unavailable for Mango))

Note: Remember Scala is a statically and strongly typed language, as such mixing Int and String as yu wanted would have resulted in a List[(String, Any)] which you never want, rather I converted all the prices to Strings before the zip.

(How to convert this auxiliary function to an extension method is left as an exercise to the reader)


You can see the code running here.


PS: If you use cats you can rather use align and the map to have better control over everything.

import cats.data.Ior
import cats.syntax.all._

val result = (prices align fruits).map {
  case Ior.Both(price, fruit) => s"${fruit} price is: ${price}"
  case Ior.Right(fruit) => s"Price Unavailable for ${fruit}"
  case Ior.Left(_) => "Undefined Fruit"
}
// result: List[String] = List("Guava price is: 5", "Banana price is: 10", "Papaya price is: 15", "Apple price is: 20", "Price Unavailable for Mango")

Thanks to Jasper to point out we can combine the previous align map with just:

val result = prices.alignWith(fruits) {
  case Ior.Both(price, fruit) => s"${fruit} price is: ${price}"
  case Ior.Right(fruit) => s"Price Unavailable for ${fruit}"
  case Ior.Left(_) => "Undefined Fruit"
}
  • Related