Emil Kantis
03/14/2023, 10:42 PMEitherNel<CreateInvoiceError, A>
)
listOfNotNull(
if (dueDate <= invoiceDate) CreateInvoiceError.InvalidDueDate(invoiceDate, dueDate) else null,
if (rows.isEmpty()) CreateInvoiceError.NoRows else null,
).toNonEmptyListOrNull()?.let { raise(it) }
simon.vergauwen
03/15/2023, 8:03 AMalpha
(planned for 1.2.0(-RC) / 2.0.0).
https://github.com/arrow-kt/arrow/blob/main/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Iterable.kt#L586
Not sure if it's really better..
listOfNotNull(
...
).flattenOrAccumulate().bind()
simon.vergauwen
03/15/2023, 8:05 AMzipOrAccumulate(
{ ensure(dueDate > invoiceDate) { CreateInvoiceError.InvalidDueDate(invoiceDate, dueDate) } },
{ ensure(rows.isNotEmpty()) { CreateInvoiceError.NoRows } }
) { _, _ -> }
Would also achieve the same thing.Emil Kantis
03/15/2023, 8:40 AMensureAll {
ensure(dueDate > invoiceDate) { CreateInvoiceError.InvalidDueDate(invoiceDate, dueDate) } }
ensure(rows.isNotEmpty()) { CreateInvoiceError.NoRows } }
}
simon.vergauwen
03/15/2023, 8:44 AMensure
would here not be implemented through raise
, and would raise
(& bind
) thus not be available here? I guess we could forbid raise
being called inside this nested DSL by using @DslMarker
support.simon.vergauwen
03/15/2023, 8:45 AMsimon.vergauwen
03/15/2023, 8:46 AMflattenOrAccumulate().bind()
could exist as ensureAll()
.
listOfNotNull(
...
).ensureAll()
Emil Kantis
03/15/2023, 8:48 AMlistOf
as well, right? πsimon.vergauwen
03/15/2023, 8:49 AMEmil Kantis
03/15/2023, 8:51 AMensureAll {}
doesn't fit.
I'll try adding ensureAll
as an alias for flattenOrAccumulate().bind()
and see how it feels π many thanks!simon.vergauwen
03/15/2023, 9:23 AMdave08
03/15/2023, 10:00 AMdave08
03/15/2023, 10:02 AMdave08
03/15/2023, 10:03 AMsimon.vergauwen
03/15/2023, 10:16 AMbind
, but it allows a special DSL that makes Validated
obsolete. It's used in APIs zipOrAccumulate
and mapOrAccumulate
and allows binding both E
and NonEmptyList<E>
in a singular API whilst providing accumulating behavior.
listOf(1, 2, 3, 4).mapOrAccumulate { i ->
when(i) {
1 -> "Either - $i".left().bind()
2 -> "EitherNel - $i".leftNel().bindNel()
3 -> raise("Raise - $i")
}
} shouldBe nonEmptyListOf("Either - 1", "EitherNel - 2", "Raise - 3").left()
dave08
03/15/2023, 10:20 AM.validate { ... }
, only that it would work with an object root that has optics on it to simplify validation against deeper values... it seems like raise()
in that context DOES accumulate lefts...?dave08
03/15/2023, 10:21 AMval foo = Foo(...)
val result: Either<Nel<ValidationError>, Foo> = foo.validate { // This could be a RaiseAccumulate scope
Foo.baz ensureThat({ it == 20 }) { BazError(it) }
Foo.bar.bara.barb ensureThat({ it.startsWith("something") }) { BarbError(it) }
} shouldBe nonEmptyListOf(BazError.., BarbError...).left()
dave08
03/15/2023, 10:27 AMcopy { }
also with the ability to use inside(...)
just like in copy { }
, but for validation...dave08
03/15/2023, 10:28 AMsimon.vergauwen
03/15/2023, 10:29 AMit seems likeMaybe I misunderstood you but in this case only the first one is accumulatedin that context DOES accumulate lefts...?raise()
listOf(10, 20).mapOrAccumulate {
raise("fail: $it")
raise("fail: ${it + 1}")
} // Either.Left(NonEmptyList("fail: 10", "fail: 20"))
dave08
03/15/2023, 10:29 AM?Copy codeshouldBe nonEmptyListOf("Either - 1", "EitherNel - 2", "Raise - 3")
dave08
03/15/2023, 10:30 AMit seems like both are thereCopy codeEither.Left(NonEmptyList("fail: 10", "fail: 20"))
dave08
03/15/2023, 10:30 AMdave08
03/15/2023, 10:31 AMdave08
03/15/2023, 10:31 AMsimon.vergauwen
03/15/2023, 10:32 AMraise
is accumulated, otherwise it would be Either.Left(NonEmptyList("fail: 10", "fail: 11", "fail: 20", "fail: 21"))
It's not possible for code to continue past raise
, or bind
. This breaks semantics of sequential imperative coding.simon.vergauwen
03/15/2023, 10:33 AMensureThat
always returns Unit
and never returns a result. In contrast of raise
and bind
, they return respectively Nothing
and A
.dave08
03/15/2023, 10:33 AMsimon.vergauwen
03/15/2023, 10:34 AMlistOf(10, 20).mapOrAccumulate { //<- accumulates all "short-circuits"
raise("fail: $it") // <- this one "short-circuits"
raise("fail: ${it + 1}") // <- this is never called
} // Either.Left(NonEmptyList("fail: 10", "fail: 20"))
dave08
03/15/2023, 10:36 AMvalidate { }
wouldn't work... I don't mind that ensureThat
returns Unit
, since the result of validate { }
is either a valid object on Right, or all the validation errors on Left....dave08
03/15/2023, 10:37 AMdave08
03/15/2023, 10:39 AMdave08
03/15/2023, 10:39 AMdave08
03/15/2023, 10:40 AMsimon.vergauwen
03/15/2023, 10:44 AMtransform: Raise<E>.(A) -> B
lambdas passed to map
, rather than map { f(a).bind() }
. In addition it also allows calling bindNel
on EitherNel<E, A>
.
Why not just accumulate all the errors?Consider following sequential computation.
fun Raise<String>.x(): Int = raise("fail")
listOf(...).mapOrAccumulate {
val x: Int = x()
ensure(x % 2 == 0) { "$x is not even" }
x + 1
}
It's not possible to accumulate "fail" and "$x is not even" if the second operation relies on the result of the first one. This completely breaks the semantics of sequential code.dave08
03/15/2023, 10:47 AMbindNel()
is what I was thinking of, and it doesn't have that problem?simon.vergauwen
03/15/2023, 10:47 AMlistOf(1, 2).mapOrAccumulate {
if(it == 1) {
"fail-1".left().bind() // <-- short-circuits "current op"
"fail-1".left().bind() // this is not accumulated
} else {
listOf("fail-2".left(), "fail-2".left()).bindAll() // both get accumulated
}
} // Either.Left(NonEmptyList("fail-1", "fail-2", "fail-2"))
simon.vergauwen
03/15/2023, 10:48 AMsimon.vergauwen
03/15/2023, 10:49 AMlistOf(
"fail-1".left(),
"fail-2".left()
).flattenOrAccumulate()
// Either.Left(NonEmptyList("fail-1", "fail-2"))
simon.vergauwen
03/15/2023, 10:49 AMbindNel
is used to combine Either<NonEmptyList<E>, A>
which is the result of these operations, so you can conveniently split -and compose smaller and bigger pieces of a very large validation program.dave08
03/15/2023, 10:51 AMlistOf("fail-2".left(), "fail-2".left()).bindAll()
is that supposed to be bindNel()
?simon.vergauwen
03/15/2023, 10:54 AMval x: Either<NonEmptyList<String>, Int> = listOf(1, 2, 3, 4).mapOrAccumulate {
ensure(it % 2 == 0) { "$it is not even" }
} // Either.Left(NonEmptyList("1 is not even", "3 is not even"))
listOf(1, 2).mapOrAccumulate {
if(it == 1) x.bindNel()
else listOf("fail-2".left(), "fail-2".left()).bindAll()
} // Either.Left(NonEmptyList("1 is not even", "3 is not even", "fail-2", "fail-2"))
dave08
03/15/2023, 10:56 AMvalidate { }
wouldn't be able to use any of all that... do you think it would be a good idea to add to Arrow? If not, how WOULD I implement it?dave08
03/15/2023, 10:57 AMdave08
03/15/2023, 10:57 AMsimon.vergauwen
03/15/2023, 10:59 AMFred Friis
03/15/2023, 11:32 AMsimon.vergauwen
03/15/2023, 11:36 AMval x: List<Int> =
listOf(1.some(), 2.some(), none<Int>())
.sequence(Option.applicative())
.getOrElse { emptyList() }
You can do the same with Validated
or with the new APis flattenOrAccumulate
.
val x: List<Int> =
listOf(1.right(), 2.right(), "fail-1".left(), "fail-2".left())
.flattenOrAccumulate()
.onLeft(::println) // NonEmptyList("fail-1", "fail-2")
.getOrElse { _: NonEmptyList<String> -> emptyList() }