Фильтрация списков в Scala Монокле

С учетом следующего кода:

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 в этом коротком примере.

Это кажется настолько практичным и распространенным, что я предполагаю, что кто-то подумал об этом, и я (с моим, я должен признать ограниченное понимание вопроса) не понимаю, почему это невозможно.

Мне не хватает этой функции, она еще не реализована или просто невозможна по какой-либо причине (пожалуйста, объясните, есть ли у вас время).

Спасибо.

Ответ 1

Плохая версия

Я начну с наивной попытки написать что-то вроде этого. Я использую простую версию списка здесь, но вы могли бы получить fancier (с 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 => A и g: A => A, t.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 в приличной библиотеке линз.

Ответ 2

Вы можете использовать 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))
  }

}