在 Scala 的 Monocle 中过滤列表
Filtering Lists in Scala's Monocle
给定以下代码:
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
。
这看起来如此实用和普遍,以至于我假设有人已经考虑过这一点,而我(我必须承认我对此事的理解有限)不明白为什么不能这样做。
我是否缺少此功能,它是否尚未实现或出于某种原因显然不可能(如果您有时间请解释)?
谢谢。
一个糟糕的版本
我将从天真的尝试开始写这样的东西。我在这里使用的是一个简单的列表版本,但如果你愿意,你可以变得更漂亮(使用 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.
好的,更好!
为什么还是不好
"real" laws for Traversal
aren't encoded in Monocle's TraversalLaws
(at least not at the moment),我们还希望有这样的东西:
For any f: A => A
and g: A => A
, t.modify(f.compose(g))
should equal 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
。
可以使用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))
}
}
给定以下代码:
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
。
这看起来如此实用和普遍,以至于我假设有人已经考虑过这一点,而我(我必须承认我对此事的理解有限)不明白为什么不能这样做。
我是否缺少此功能,它是否尚未实现或出于某种原因显然不可能(如果您有时间请解释)?
谢谢。
一个糟糕的版本
我将从天真的尝试开始写这样的东西。我在这里使用的是一个简单的列表版本,但如果你愿意,你可以变得更漂亮(使用 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.
好的,更好!
为什么还是不好
"real" laws for Traversal
aren't encoded in Monocle's TraversalLaws
(at least not at the moment),我们还希望有这样的东西:
For any
f: A => A
andg: A => A
,t.modify(f.compose(g))
should equalt.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
。
可以使用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))
}
}