是否可以在 Scala 中默认替换 ZipAll 时传递动态值?

Is it possible to pass dynamic values in default replacement of ZipAll in Scala?

我对两个大小不等的列表使用 zipAll,如下所示:

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

我正在使用 zipAll 如下图:

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

产生以下结果:

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

我感兴趣的是传递动态默认填充符;所以在这种情况下,我期待的是:

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

哪个应该产生:

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

由于stdlib没有提供这样的功能,所以需要自己实现:

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) 
    }

虽然,这个很贵。
您可以使用尾递归函数提高操作效率;或者你可以使用 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

那么你可以这样使用它:

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.

(如何将此辅助函数转换为扩展方法留作reader的练习)


可以看到代码运行 here.


PS:如果你使用 cats,你可以使用 alignmap 来更好地控制一切。

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")

感谢 Jasper 指出我们可以将前面的 align + map

结合起来
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"
}