Is there any better built-in options for achieving...
# arrow
e
Is there any better built-in options for achieving the same results as this? (Arrow 2.0, doing input validation in a function with type
EitherNel<CreateInvoiceError, A>
)
Copy code
listOfNotNull(
         if (dueDate <= invoiceDate) CreateInvoiceError.InvalidDueDate(invoiceDate, dueDate) else null,
         if (rows.isEmpty()) CreateInvoiceError.NoRows else null,
      ).toNonEmptyListOrNull()?.let { raise(it) }
s
I guess there is this on
alpha
(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..
Copy code
listOfNotNull(
  ...
).flattenOrAccumulate().bind()
Or:
Copy code
zipOrAccumulate(
  { ensure(dueDate > invoiceDate) { CreateInvoiceError.InvalidDueDate(invoiceDate, dueDate) } },
  { ensure(rows.isNotEmpty()) { CreateInvoiceError.NoRows } }
) { _, _ -> }
Would also achieve the same thing.
e
What do you think of adding something like this?
Copy code
ensureAll {
  ensure(dueDate > invoiceDate) { CreateInvoiceError.InvalidDueDate(invoiceDate, dueDate) } }
  ensure(rows.isNotEmpty()) { CreateInvoiceError.NoRows } }
}
s
Such a DSL could be implemented but it'd be a bit strange πŸ€” I.e.
ensure
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.
It feels like it breaks sequentiality, which isn't intuitive.
Perhaps
flattenOrAccumulate().bind()
could exist as
ensureAll()
.
Copy code
listOfNotNull(
  ...
).ensureAll()
e
And I guess it could just be
listOf
as well, right? πŸ™‚
s
Ye, although I am not getting a bit lost πŸ˜…
e
I still struggle a bit with figuring out how to do things "the Arrow way" πŸ˜„ so I trust you when you say
ensureAll {}
doesn't fit. I'll try adding
ensureAll
as an alias for
flattenOrAccumulate().bind()
and see how it feels πŸ™‚ many thanks!
s
My pleasure @Emil Kantis ☺️ I wouldn't focus too much on "the Arrow way" 😁 We try to be opinionated, and un-opinionated at the same time πŸ˜‚ With that I mean that Arrow should not be invasive to your existing programming style, while still enabling all the same patterns/features that you can find in more "hardcore" FP languages as Arrow tried to originally model 6 years ago. I'm going to do another push to get the preview of the new website out today, that is focused around use-case oriented tutorial style documentation πŸ˜‰ Besides that I think these are the best examples: β€’ Ktor Example β€’ Ktor Example with Context Receivers β€’ Github Alerts Subscriptions β€’ Github Alerts Subscriptions with Context Receivers (Biased I worked on all of them πŸ˜…)
d
Just a little ping on something similar I tried to suggest: https://kotlinlang.slack.com/archives/C5UPMM0A0/p1677678003957219
But there I was thinking that optics could make multi-level validation even simpler...
Btw, what's RaiseAccumulate? I saw it in the source code... is it some kind of Raise scope that accumulates instead of short-circuiting on Left?
s
That is not possible due to the signature of
bind
, 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.
Copy code
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()
d
Yeah, that's what I meant for
.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...?
Something like:
Copy code
val 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()
a little bit like
copy { }
also with the ability to use
inside(...)
just like in
copy { }
, but for validation...
This could make validating complex structures MUCH more elegant.
s
it seems like
raise()
in that context DOES accumulate lefts...?
Maybe I misunderstood you but in this case only the first one is accumulated
Copy code
listOf(10, 20).mapOrAccumulate {
  raise("fail: $it")
  raise("fail: ${it + 1}")
} // Either.Left(NonEmptyList("fail: 10", "fail: 20"))
d
Copy code
shouldBe nonEmptyListOf("Either - 1", "EitherNel - 2", "Raise - 3")
?
Copy code
Either.Left(NonEmptyList("fail: 10", "fail: 20"))
it seems like both are there
not just the first one
Or in the former example all three are in the Nel, and raise doesn't just short-circuit the former ones
But adds itself to the Nel
s
Only the first
raise
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.
In the case of your DSL, it's possible to implement but only if
ensureThat
always returns
Unit
and never returns a result. In contrast of
raise
and
bind
, they return respectively
Nothing
and
A
.
d
Oh... you mean out of the values in the list... I meant the raises inside the block
s
I'm confused now @dave08.
Copy code
listOf(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"))
d
I would have though that only Left("fail 10") would be raised and the second one skipped... then my
validate { }
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....
That's very confusing in mapOrAccumulate, now that I think of it, because Accumulate could mean multiple things πŸ˜΅β€πŸ’«
Oooh, I looked at your example again, it's the opposite of what I thought...!
It short circuits on each value in the list... why?
Why not just accumulate all the errors?
s
It refers to accumulating all
transform: 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.
Copy code
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.
d
So
bindNel()
is what I was thinking of, and it doesn't have that problem?
s
I think you want this:
Copy code
listOf(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"))
Where you can define non-sequential validations, and get all of them?
You can also do:
Copy code
listOf(
  "fail-1".left(),
  "fail-2".left()
).flattenOrAccumulate()

// Either.Left(NonEmptyList("fail-1", "fail-2"))
bindNel
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.
d
Copy code
listOf("fail-2".left(), "fail-2".left()).bindAll()
is that supposed to be
bindNel()
?
s
No, those are still a bit different.
Copy code
val 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"))
d
So I guess my
validate { }
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?
Short of validate(vararg...)
A DSL would be a bit easier to work with
s
I can share a small snippet with an implementation after lunch πŸ‘
f
late to the party but in the past I've used applicative of Option to turn a list of Option<Foo> into either a list of Foo, or, if a single Option is empty, an empty list could the same not be an alternative for this use case? πŸ€” I don't have the code in front of me but unless I'm misremembering it wasn't ugly or opaque
s
That was probably something like:
Copy code
val 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
.
Copy code
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() }
102 Views