Given the following code:
case class Person(name :String)
case class Group(group :List[Person])
val personLens = GenLens[Person]
val groupLens = GenLens[Group]
how can i "filter" out certain Persons from the selection, NOT by index but by a specific property of Person
, like:
val trav :Traversal[Group, Person] = (groupLens(_.group) composeTraversal filterWith((x :Person) => /*expression of type Boolean here */))
I only found the filterIndex
function, which does only include elements from the list based on the index, but this is not what I want.
filterIndex
takes a function of type: (Int => Boolean)
and I want:
filterWith
(made up name), that takes a (x => Boolean)
, where x has the type of the element of the list, namely Person
in this short example.
This seems so practical and common that I assume somebody has thought about that and i (with my, i must admit limited understanding of the matter) don't see why it can't be done.
Am I missing this functionality, is it not implemented yet or just plainly impossible for whatever reason (please do explain if you have the time)?
Thank you.
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!
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.
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!
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
andg: A => A
,t.modify(f.compose(g))
should equalt.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.
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.