Scala的Monocle中的列表过滤

10

考虑以下代码:

case class Person(name :String)
case class Group(group :List[Person])

val personLens = GenLens[Person]
val groupLens = GenLens[Group]

我如何根据Person的特定属性,例如:

"过滤"出选择中的某些人,而不是通过索引来进行过滤?

val trav :Traversal[Group, Person] = (groupLens(_.group) composeTraversal filterWith((x :Person) => /*expression of type Boolean here */))

我只发现了filterIndex函数,它仅基于索引包含列表中的元素,但这不是我想要的。

filterIndex使用类型为:(Int => Boolean)的函数

而我需要:

filterWith(虚构名称),它接受一个(x => Boolean),其中x具有列表元素的类型,即在此简短示例中的Person

这似乎非常实用和常见,我认为有人已经考虑过这一点,但我(必须承认我的理解有限)不知道为什么不能这样做。

我是否错过了这个功能,它尚未实现,还是出于某种原因根本不可能(如果您有时间,请解释一下)?

谢谢。

2个回答

17

一个糟糕的版本

我将开始尝试编写类似于这样的东西。我在这里使用了一个简单的列表版本,但如果您愿意,您可以使用更高级的方式(例如Traverse)。

import monocle.Traversal
import scalaz.Applicative, scalaz.std.list._, scalaz.syntax.traverse._

def filterWith[A](p: A => Boolean): Traversal[List[A], A] =
  new Traversal[List[A], A] {
    def modifyF[F[_]: Applicative](f: A => F[A])(s: List[A]): F[List[A]] =
      s.filter(p).traverse(f)
  }

然后:

import monocle.macros.GenLens

case class Person(name: String)
case class Group(group: List[Person])

val personLens = GenLens[Person]
val groupLens = GenLens[Group]

val aNames = groupLens(_.group).composeTraversal(filterWith(_.name.startsWith("A")))

val group = Group(List(Person("Al"), Person("Alice"), Person("Bob")))

最后:

scala> aNames.getAll(group)
res0: List[Person] = List(Person(Al), Person(Alice))

它运作了!


为什么它不好

它可以工作,但是...

scala> import monocle.law.discipline.TraversalTests
import monocle.law.discipline.TraversalTests

scala> TraversalTests(filterWith[String](_.startsWith("A"))).all.check
+ Traversal.get what you set: OK, passed 100 tests.
+ Traversal.headOption: OK, passed 100 tests.
! Traversal.modify id = id: Falsified after 2 passed tests.
> Labels of failing property: 
Expected List(崡) but got List()
> ARG_0: List(崡)
! Traversal.modifyF Id = Id: Falsified after 2 passed tests.
> Labels of failing property: 
Expected List(ᜱ) but got List()
> ARG_0: List(ᜱ)
+ Traversal.set idempotent: OK, passed 100 tests.

三分之二不是很好。

稍微好一点的版本

让我们重新开始:

def filterWith2[A](p: A => Boolean): Traversal[List[A], A] =
  new Traversal[List[A], A] {
    def modifyF[F[_]: Applicative](f: A => F[A])(s: List[A]): F[List[A]] =
      s.traverse {
        case a if p(a) => f(a)
        case a => Applicative[F].point(a)
      }
  }

val aNames2 = groupLens(_.group).composeTraversal(filterWith2(_.name.startsWith("A")))

然后:

scala> aNames2.getAll(group)
res1: List[Person] = List(Person(Al), Person(Alice))

scala> TraversalTests(filterWith2[String](_.startsWith("A"))).all.check
+ Traversal.get what you set: OK, passed 100 tests.
+ Traversal.headOption: OK, passed 100 tests.
+ Traversal.modify id = id: OK, passed 100 tests.
+ Traversal.modifyF Id = Id: OK, passed 100 tests.
+ Traversal.set idempotent: OK, passed 100 tests.

好的,更好了!


为什么它仍然不好

Traversal"真实"法则没有编码在Monocle的TraversalLaws中(至少目前还没有),而我们还希望类似于下面这样的内容能够保持:

对于任何f:A => Ag:A => At.modify(f.compose(g))应该等于t.modify(f).compose(t.modify(g))

让我们来试一试:

scala> val graduate: Person => Person = p => Person("Dr. " + p.name)
graduate: Person => Person = <function1>

scala> val kill: Person => Person = p => Person(p.name + ", deceased")
kill: Person => Person = <function1>

scala> aNames2.modify(kill.compose(graduate))(group)
res2: Group = Group(List(Person(Dr. Al, deceased), Person(Dr. Alice, deceased), Person(Bob)))

scala> aNames2.modify(kill).compose(aNames2.modify(graduate))(group)
res3: Group = Group(List(Person(Dr. Al), Person(Dr. Alice), Person(Bob)))

所以我们又没有运气了。我们的filterWith合法的唯一方式是承诺永远不使用可能改变谓词结果的参数来调用modify
这就是为什么filterIndex是合法的——它的谓词以一个modify无法触及的参数作为参数,因此您无法违反t.modify(f.compose(g)) === t.modify(f).compose(t.modify(g))法则。

故事寓意

你可以编写一种不合法的 Traversal,用它进行非法过滤操作,并且经常使用它,很有可能不会对你造成伤害,也不会让人认为你是一个可怕的人。如果你想这么做,就去做吧。不过,在一个好的镜头中,你很可能永远看不到 filterWith


1
谢谢。非常好的答案! - Lazarus535

2
你可以使用 UnsafeSelect,https://www.optics.dev/Monocle/docs/unsafe_module.html#unsafeselect。最初的回答。
import monocle.macros.GenLens
import org.scalatest.FunSuite
import monocle.function.all._
import monocle.unsafe.UnsafeSelect

case class Person(name :String, age: Int)
case class Group(group :List[Person])


class Example extends FunSuite{

  test("filter elements of list") {

    val group = Group(List(Person("adult1", 2), Person("adult2", 3), Person("child", 4)))

    val filteredGroup = (GenLens[Group](_.group) composeTraversal each composePrism UnsafeSelect.unsafeSelect(_.name.startsWith("adult")) composeLens GenLens[Person](_.age) set 18) (group)

    assert(filteredGroup.group.filter(_.name.startsWith("adult")).map(_.age) == List(18, 18))
  }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接