Filtering Lists in Scala's Monocle

杀马特。学长 韩版系。学妹 提交于 2019-12-05 01:58:07

A bad version

I'll start with a naive attempt to write something like this. I'm using a simple list version here, but you could get fancier (with Traverse or whatever) if you wanted.

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

And then:

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

And finally:

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

It works!


Why it's bad

It works, except…

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.

Three out of five isn't very good.


A slightly better version

Let's start over:

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

And then:

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.

Okay, better!


Why it's still bad

The "real" laws for Traversal aren't encoded in Monocle's TraversalLaws (at least not at the moment), and we additionally want something like this to hold:

For any f: A => A and g: A => A, t.modify(f.compose(g)) should equal t.modify(f).compose(t.modify(g)).

Let's try it:

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

So we're out of luck again. The only way our filterWith could actually be lawful is if we promise never to use it with an argument to modify that might change the result of the predicate.

This is why filterIndex is legit—its predicate takes as an argument something that modify can't touch, so you can't break the t.modify(f.compose(g)) === t.modify(f).compose(t.modify(g)) law.


Moral of the story

You could write an unlawful Traversal that does unlawful filtering stuff and use it all the time and it's pretty likely that it will never hurt you and that nobody will ever think you are a horrible person. So go for it, if you want. You'll probably never see a filterWith in a decent lens library, though.

You can use UnsafeSelect, https://julien-truffaut.github.io/Monocle/unsafe_module.html

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

}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!