Фильтрация списков в монокле Скалы
Учитывая следующий код:
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)
где х имеет тип элемента списков, а именно Person
в этом коротком примере.
Это кажется настолько практичным и распространенным, что я предполагаю, что кто-то думал об этом, и я (с моим, я должен признать ограниченное понимание вопроса) не понимаю, почему это не может быть сделано.
Мне не хватает этой функциональности, она еще не реализована или просто невозможна по какой-либо причине (пожалуйста, объясните, если у вас есть время).
Спасибо.
1 ответ
Плохая версия
Начну с наивной попытки написать что-то подобное. Я использую простую версию списка, но вы можете получить более 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
не кодируются в монокль 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
в приличной библиотеке линз, хотя.
Вы можете использовать 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))
}
}