用于更好地链接收集的功能模式

Jac*_*nig 5 functional-programming scala

我经常发现自己需要collects在我想要在一次遍历中进行多次收集的地方进行链接。对于与任何集合都不匹配的东西,我还想返回一个“剩余部分”。

例如:

sealed trait Animal
case class Cat(name: String) extends Animal
case class Dog(name: String, age: Int) extends Animal

val animals: List[Animal] =
  List(Cat("Bob"), Dog("Spot", 3), Cat("Sally"), Dog("Jim", 11))

// Normal way
val cats: List[Cat]    = animals.collect { case c: Cat => c }
val dogAges: List[Int] = animals.collect { case Dog(_, age) => age }
val rem: List[Animal]  = Nil // No easy way to create this without repeated code
Run Code Online (Sandbox Code Playgroud)

这真的不是很好,它需要多次迭代并且没有合理的方法来计算余数。我可以写一个非常复杂的折叠来解决这个问题,但这真的很讨厌。

相反,我通常会选择与您在 a 中拥有的逻辑非常相似的突变fold

import scala.collection.mutable.ListBuffer

// Ugly, hide the mutation away
val (cats2, dogsAges2, rem2) = {
  // Lose some benefits of type inference
  val cs = ListBuffer[Cat]()
  val da = ListBuffer[Int]()
  val rem = ListBuffer[Animal]()
  // Bad separation of concerns, I have to merge all of my functions
  animals.foreach {
    case c: Cat      => cs += c
    case Dog(_, age) => da += age
    case other       => rem += other
  }
  (cs.toList, da.toList, rem.toList)
}
Run Code Online (Sandbox Code Playgroud)

我不喜欢这一点,它具有更糟糕的类型推断和关注点分离,因为我必须合并所有不同的部分函数。它还需要很多行代码。

我想要的是一些有用的模式,比如collect返回余数的 a (我partitionMap在 2.13 中授予new 这样做,但更丑陋)。我也可以使用某种形式的pipemap用于操作元组的一部分。以下是一些组成的实用程序:

implicit class ListSyntax[A](xs: List[A]) {
  import scala.collection.mutable.ListBuffer
  // Collect and return remainder
  // A specialized form of new 2.13 partitionMap
  def collectR[B](pf: PartialFunction[A, B]): (List[B], List[A]) = {
    val rem = new ListBuffer[A]()
    val res = new ListBuffer[B]()
    val f = pf.lift
    for (elt <- xs) {
      f(elt) match {
        case Some(r) => res += r
        case None    => rem += elt
      }
    }
    (res.toList, rem.toList)
  }
}
implicit class Tuple2Syntax[A, B](x: Tuple2[A, B]){
  def chainR[C](f: B => C): Tuple2[A, C] = x.copy(_2 = f(x._2))
}
Run Code Online (Sandbox Code Playgroud)

现在,我可以用一种可以在单次遍历中完成的方式(使用惰性数据结构)来编写它,并且遵循功能性的、不可变的实践:

// Relatively pretty, can imagine lazy forms using a single iteration
val (cats3, (dogAges3, rem3)) =
  animals.collectR          { case c: Cat => c }
         .chainR(_.collectR { case Dog(_, age) => age })
Run Code Online (Sandbox Code Playgroud)

我的问题是,有这样的模式吗?它闻起来有点像 Cats、FS2 或 ZIO 这样的库中的东西,但我不确定它可能叫什么。

代码示例的 Scastie 链接:https ://scastie.scala-lang.org/Egz78fnGR6KyqlUTNTv9DQ

jwv*_*wvh 7

我想看看 afold()会有多“讨厌” 。

val (cats
    ,dogAges
    ,rem) = animals.foldRight((List.empty[Cat]
                              ,List.empty[Int]
                              ,List.empty[Animal])) {
  case (c:Cat,   (cs,ds,rs)) => (c::cs, ds, rs)
  case (Dog(_,d),(cs,ds,rs)) => (cs, d::ds, rs)
  case (r,       (cs,ds,rs)) => (cs, ds, r::rs)
}
Run Code Online (Sandbox Code Playgroud)

我想是旁观者的眼睛。


use*_*ser 4

定义几个实用程序类来帮助您解决这个问题怎么样?

case class ListCollect[A](list: List[A]) {
  def partialCollect[B](f: PartialFunction[A, B]): ChainCollect[List[B], A] = {
    val (cs, rem) = list.partition(f.isDefinedAt)
    new ChainCollect((cs.map(f), rem))
  }
}

case class ChainCollect[A, B](tuple: (A, List[B])) {
  def partialCollect[C](f: PartialFunction[B, C]): ChainCollect[(A, List[C]), B] = {
    val (cs, rem) = tuple._2.partition(f.isDefinedAt)
    ChainCollect(((tuple._1, cs.map(f)), rem))
  }
}
Run Code Online (Sandbox Code Playgroud)

ListCollect只是为了启动链,并ChainCollect获取先前的余数(元组的第二个元素)并尝试将 a 应用于PartialFunction它,创建一个新ChainCollect对象。我不是特别喜欢这种生成的嵌套元组,但是如果您使用 Shapeless ,也许可以让它看起来更好一些HList

val ((cats, dogs), rem) = ListCollect(animals)
  .partialCollect { case c: Cat => c }
  .partialCollect { case Dog(_, age) => age }
  .tuple
Run Code Online (Sandbox Code Playgroud)

斯卡斯蒂


Dotty 的*:类型让这变得更容易一些:

opaque type ChainResult[Prev <: Tuple, Rem] = (Prev, List[Rem])

extension [P <: Tuple, R, N](chainRes: ChainResult[P, R]) {
  def partialCollect(f: PartialFunction[R, N]): ChainResult[List[N] *: P, R] = {
    val (cs, rem) = chainRes._2.partition(f.isDefinedAt)
    (cs.map(f) *: chainRes._1, rem)
  }
}
Run Code Online (Sandbox Code Playgroud)

这确实最终导致输出被反转,但它没有我之前的方法中那种丑陋的嵌套:


val ((owls, dogs, cats), rem) = (EmptyTuple, animals)
  .partialCollect { case c: Cat => c }
  .partialCollect { case Dog(_, age) => age }
  .partialCollect { case Owl(wisdom) => wisdom }

/* more animals */

case class Owl(wisdom: Double) extends Animal
case class Fly(isAnimal: Boolean) extends Animal

val animals: List[Animal] =
  List(Cat("Bob"), Dog("Spot", 3), Cat("Sally"), Dog("Jim", 11), Owl(200), Fly(false))
Run Code Online (Sandbox Code Playgroud)

斯卡斯蒂

如果您仍然不喜欢这样,您可以随时定义更多辅助方法来反转元组,在列表上添加扩展而无需从 EmptyTuple 开始,等等。

//Add this to the ChainResult extension
def end: Reverse[List[R] *: P] = {
    def revHelp[A <: Tuple, R <: Tuple](acc: A, rest: R): RevHelp[A, R] =
      rest match {
        case EmptyTuple => acc.asInstanceOf[RevHelp[A, R]]
        case h *: t => revHelp(h *: acc, t).asInstanceOf[RevHelp[A, R]]
      }
    revHelp(EmptyTuple, chainRes._2 *: chainRes._1)
  }

//Helpful types for safety
type Reverse[T <: Tuple] = RevHelp[EmptyTuple, T]
type RevHelp[A <: Tuple, R <: Tuple] <: Tuple = R match {
  case EmptyTuple => A
  case h *: t => RevHelp[h *: A, t]
}
Run Code Online (Sandbox Code Playgroud)

现在你可以这样做:

val (cats, dogs, owls, rem) = (EmptyTuple, animals)
  .partialCollect { case c: Cat => c }
  .partialCollect { case Dog(_, age) => age }
  .partialCollect { case Owl(wisdom) => wisdom }
  .end
Run Code Online (Sandbox Code Playgroud)

斯卡斯蒂