diff --git a/arrow-libs/optics/arrow-optics/api/arrow-optics.api b/arrow-libs/optics/arrow-optics/api/arrow-optics.api index b7797074784..3fecfcd3878 100644 --- a/arrow-libs/optics/arrow-optics/api/arrow-optics.api +++ b/arrow-libs/optics/arrow-optics/api/arrow-optics.api @@ -447,6 +447,7 @@ public abstract interface class arrow/optics/POptional : arrow/optics/Fold, arro public static final field Companion Larrow/optics/POptional$Companion; public abstract fun choice (Larrow/optics/POptional;)Larrow/optics/POptional; public abstract fun compose (Larrow/optics/POptional;)Larrow/optics/POptional; + public static fun filter (Lkotlin/jvm/functions/Function1;)Larrow/optics/POptional; public abstract fun first ()Larrow/optics/POptional; public abstract fun foldMap (Larrow/typeclasses/Monoid;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public abstract fun getOrModify (Ljava/lang/Object;)Larrow/core/Either; @@ -463,6 +464,7 @@ public abstract interface class arrow/optics/POptional : arrow/optics/Fold, arro public final class arrow/optics/POptional$Companion { public final fun codiagonal ()Larrow/optics/POptional; + public final fun filter (Lkotlin/jvm/functions/Function1;)Larrow/optics/POptional; public final fun id ()Larrow/optics/PIso; public final fun invoke (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Larrow/optics/POptional; public final fun listHead ()Larrow/optics/POptional; diff --git a/arrow-libs/optics/arrow-optics/src/commonMain/kotlin/arrow/optics/Optional.kt b/arrow-libs/optics/arrow-optics/src/commonMain/kotlin/arrow/optics/Optional.kt index fba07527fb5..549d60a9f3f 100644 --- a/arrow-libs/optics/arrow-optics/src/commonMain/kotlin/arrow/optics/Optional.kt +++ b/arrow-libs/optics/arrow-optics/src/commonMain/kotlin/arrow/optics/Optional.kt @@ -204,5 +204,42 @@ public interface POptional : PSetter, Fold, PTrave getOption = { if (it.isEmpty()) None else Some(it.drop(1)) }, set = { list, newTail -> if (list.isNotEmpty()) list[0] prependTo newTail else emptyList() } ) + + /** + * [Optional] to itself if it satisfies the predicate. + * + * Select all the elements which satisfies the predicate. + * ```kotlin + * import arrow.optics.Traversal + * import arrow.optics.Optional + * + * val positiveNumbers = Traversal.list() compose Optional.filter { it >= 0 } + * + * positiveNumbers.getAll(listOf(1,2,-3,4,-5)) == listOf(1,2,4) + * positiveNumbers.modify(listOf(1,2,-3,4,-5)) { it * 10 } == listOf(10,20,-3,40,-5) + *``` + * + * `filter` can break the fusion property, if `replace` or `modify` do not preserve the predicate. For example, here + * the first `modify` {`x - 3`} transform the positive number 1 into the negative number -2. + * + *```kotlin + * import arrow.optics.Traversal + * import arrow.optics.Optional + * + * val positiveNumbers = Traversal.list() compose Optional.filter { it >= 0 } + * val list = listOf(1, 5, -3) + * val firstStep = positiveNumbers.modify(list){ it - 3 } // List(-2, 2, -3) + * val secondStep = positiveNumbers.modify(firstStep) { it * 2 } // List(-2, 4, -3) + * val bothSteps = positiveNumbers.modify(list){ (it - 3) * 2) // List(-4, 4, -3) + * // secondStep != bothSteps + * ``` + */ + + @JvmStatic + public fun filter(predicate: (A) -> Boolean): Optional = + Optional( + getOption = { if (predicate(it)) Some(it) else None }, + set = { current, newValue -> if (predicate(current)) newValue else current } + ) } } diff --git a/arrow-libs/optics/arrow-optics/src/commonTest/kotlin/arrow/optics/OptionalTest.kt b/arrow-libs/optics/arrow-optics/src/commonTest/kotlin/arrow/optics/OptionalTest.kt index 0fdeb495bb2..1dbc6f81355 100644 --- a/arrow-libs/optics/arrow-optics/src/commonTest/kotlin/arrow/optics/OptionalTest.kt +++ b/arrow-libs/optics/arrow-optics/src/commonTest/kotlin/arrow/optics/OptionalTest.kt @@ -152,13 +152,15 @@ class OptionalTest : UnitSpec() { "Checking existence predicate over the target should result in same result as predicate" { checkAll(Arb.list(Arb.int().orNull()), Arb.boolean()) { list, predicate -> - Optional.listHead().exists(list) { predicate } shouldBe (predicate && list.isNotEmpty()) + Optional.listHead() + .exists(list) { predicate } shouldBe (predicate && list.isNotEmpty()) } } "Checking satisfaction of predicate over the target should result in opposite result as predicate" { checkAll(Arb.list(Arb.int()), Arb.boolean()) { list, predicate -> - Optional.listHead().all(list) { predicate } shouldBe if (list.isEmpty()) true else predicate + Optional.listHead() + .all(list) { predicate } shouldBe if (list.isEmpty()) true else predicate } } @@ -169,5 +171,37 @@ class OptionalTest : UnitSpec() { joinedOptional.getOrNull(Left(listOf(int))) shouldBe joinedOptional.getOrNull(Right(int)) } } + + "get should return value if predicate is true and null if otherwise" { + checkAll(Arb.int(), Arb.boolean()) { int, predicate -> + Optional.filter { predicate }.getOrNull(int) shouldBe (if (predicate) int else null) + } + } + + "set should return value if predicate is true and null if otherwise" { + checkAll(Arb.int(), Arb.int(), Arb.boolean()) { int, newValue, predicate -> + Optional.filter { predicate } + .set(int, newValue) shouldBe (if (predicate) newValue else int) + } + } + + "getAll should return the old list if predicate is true" { + checkAll(Arb.list(Arb.int()), Arb.boolean()) { list, predicate -> + (Fold.list() compose Optional.filter { predicate }).getAll(list) shouldBe (if (predicate) list else emptyList()) + } + } + + "set with predicate true should have the same result with map" { + checkAll(Arb.list(Arb.int()), Arb.functionAToB(Arb.int())) { list, f -> + (Traversal.list() compose Optional.filter { true }).modify(list, f) shouldBe list.map(f) + } + } + + "set with predicate false should return the old list" { + checkAll(Arb.list(Arb.int()), Arb.functionAToB(Arb.int())) { list, f -> + (Traversal.list() compose Optional.filter { false }).modify(list, f) shouldBe list + } + } + } }