Chris Lee
01/21/2024, 11:06 PM@OptIn(ExperimentalTypeInference::class)
@RaiseDSL
public inline fun <Error, A, B> Raise<Error>.mapFirstOrNull(
iterable: Iterable<A>,
@BuilderInference transform: RaiseAccumulate<Error>.(A) -> B
): B? {
val error = mutableListOf<Error>()
for (item in iterable) {
fold<NonEmptyList<Error>, B, Unit>(
{ transform(RaiseAccumulate(this), item) },
{ errors -> error.addAll(errors) },
{ return@mapFirstOrNull it }
)
}
return error.toNonEmptyListOrNull()?.let { raise(it.first()) }
}
…are there alternate mechanisms to consider for this?Youssef Shoaib [MOD]
01/22/2024, 12:32 AMwithError({ e: Nel<Error> -> e.first() }) { ... }
Is what you're looking for, and then mapOrAccumulate
on the inside.Youssef Shoaib [MOD]
01/22/2024, 12:33 AMmapOrAccumulate
where the combine function is { a, _ -> a }
Chris Lee
01/22/2024, 12:33 AMYoussef Shoaib [MOD]
01/22/2024, 12:35 AMmapOrAccumulate
, but with the raise
being changed to take the first error only. I.e. you're transforming the raised list and taking its first itemChris Lee
01/22/2024, 12:35 AMChris Lee
01/22/2024, 12:44 AMwithError
has already decided to raise the Nel<Error> list - the best we can do there is transform it, which doesn’t satisfy this use case (if there is a successful record use that, other wise raise error).
// fails: java.lang.AssertionError: Expected to succeed, but raised 'Error'
raiseTest<String>("accumulate") {
val list = listOf("a", "b", "c")
withError({ errors: Nel<String> -> errors.first() }) {
mapOrAccumulate(list) {
when (it) {
"c" -> 1
else -> raise("Error")
}
}
}.shouldBe(1)
}
// succeeds
raiseTest<String>("mapFirstOrNull") {
val list = listOf("a", "b", "c")
mapFirstOrNull(list) {
when (it) {
"c" -> 1
else -> raise("Error")
}
}.shouldBe(1)
}
Chris Lee
01/22/2024, 2:09 AMtakeWhile
on Sequence perhaps, with a takeUntil
variant (that includes the terminal element) - but handling both success and error paths.
Transforming error is orthogonal and well-handled by withError
, assuming we can control what constitutes an iteration error.Alejandro Serrano.Mena
01/22/2024, 7:56 AMpublic fun <Error, A, B> Raise<Error>.mapFirstOrNull(
iterable: Iterable<A>,
transform: Raise<Error>.(A) -> B
): B? = iterable.firstOrNull().map { transform(it) }
maybe with this you can also see that there's no need to "thread" the Raise<Error>
, since it's just moving from the context, so you could even write
public fun <A, B> Iterable<A>.mapFirstOrNull(
transform: (A) -> B
): B? = firstOrNull().map(transform)
and use it in a place in which you need a Raise<E>
without any problemAlejandro Serrano.Mena
01/22/2024, 7:57 AMmapOrAccumulate
is not the only function to operate with errors on lists, the regular map
is the one embodying the "fail first" strategyChris Lee
01/22/2024, 2:17 PMclass RaiseAccumulateTest :
FunSpec({
val list = listOf("a", "b", "c", "d", "e")
/*
Goal: return the first successful element, or the raised errors
For the above list, the first successful element is "c"; this should be returned
without raising any errors. Iteration short-circuits at "c": "d" and "e" will not be processed.
In the event there were no successful elements, all errors should be raised.
In the event the list is empty (no elements), null should be returned.
*/
// fails: java.lang.AssertionError: Expected to succeed, but raised 'Error a'
raiseTest<String>("arrow: mapOrAccumulate") {
withError({ errors: Nel<String> -> errors.first() }) {
// issue: mapOrAccumulate does not support short-circuiting iteration; all
// entries are processed
// all raised errors are re-raised as a Nel<Error>
// hence unable to short-circuit on the first successful element
mapOrAccumulate(list) {
when (it) {
"c" -> 1
else -> raise("Error $it")
}
}
}
.shouldBe(1)
}
// fails: java.lang.AssertionError: Expected to succeed, but raised 'Error a'
raiseTest<String>("custom: mapFirstOrNull2") {
mapFirstOrNull2(list) {
when (it) {
"c" -> 1
else -> raise("Error $it")
}
}
.shouldBe(1)
}
// java.lang.AssertionError: Expected to succeed, but raised 'Error a'
raiseTest<String>("custom: mapFirstOrNull3") {
list
.mapFirstOrNull3 {
when (it) {
"c" -> 1
else -> raise("Error $it")
}
}
.shouldBe(1)
}
// succeeds
raiseTest<Nel<String>>("custom: mapFirstOrNull") {
mapFirstOrNull(list) {
when (it) {
"c" -> 1
else -> raise("Error $it")
}
}
.shouldBe(1)
mapFirstOrNull(emptyList<String>()) {
when (it) {
"c" -> 1
else -> raise("Error $it")
}
}
.shouldBe(null)
}
})
// as supplied on slack, corrected with "?.let" instead of ".map"
internal fun <Error, A, B> Raise<Error>.mapFirstOrNull2(
iterable: Iterable<A>,
transform: Raise<Error>.(A) -> B
): B? = iterable.firstOrNull()?.let { transform(it) }
// as supplied on slack, corrected with "?.let" instead of ".map"
internal fun <A, B> Iterable<A>.mapFirstOrNull3(transform: (A) -> B): B? =
firstOrNull()?.let(transform)
/**
* Returns the first success (non-error-raising) result of the supplied Iterable or the raised
* errors. In the event the Iterable is empty null is returned.
*/
@OptIn(ExperimentalTypeInference::class)
@RaiseDSL
internal inline fun <Error, A, B> Raise<Nel<Error>>.mapFirstOrNull(
iterable: Iterable<A>,
@BuilderInference transform: RaiseAccumulate<Error>.(A) -> B
): B? {
val error = mutableListOf<Error>()
for (item in iterable) {
fold<NonEmptyList<Error>, B, Unit>(
{ transform(RaiseAccumulate(this), item) },
{ errors -> error.addAll(errors) },
{
return@mapFirstOrNull it
}
)
}
return error.toNonEmptyListOrNull()?.let { raise(it) }
}
Alejandro Serrano.Mena
01/22/2024, 2:23 PMRaise
of each element to "depend" on the previous one
internal fun <Error, A, B> Raise<Nel<Error>>.mapFirstOfNull(
iterable: Iterable<A>,
@BuilderInference transform: Raise<Error>.(A) -> B
) {
val errors = mutableList<Error>()
iterable.forEach { element ->
withError({ e -> errors.add(e) }) {
return@mapFirstOfNull transform(element)
}
}
return errors.toNonEmptyListOrNull()
}
Chris Lee
01/22/2024, 2:33 PM@OptIn(ExperimentalTypeInference::class)
internal fun <Error, A, B> Raise<Nel<Error>>.mapFirstOrNull4(
iterable: Iterable<A>,
@BuilderInference transform: Raise<Error>.(A) -> B
): B? {
val errors = mutableListOf<Error>()
iterable.forEach { element ->
// doesn't compile; withError must return a Raise<Error> to be raised unconditionally
// withError({ e -> errors.add(e) }) {
// return@mapFirstOrNull4 transform(element)
// }
recover({
return@mapFirstOrNull4 transform(element)
}) {
errors.add(it)
}
}
if (errors.isEmpty()) return null
raise(errors.toNonEmptyListOrNull()!!)
}
Youssef Shoaib [MOD]
01/22/2024, 2:34 PMmapOrAccumulate
where in the transform you early-return. I think that would give the desired behaviourAlejandro Serrano.Mena
01/22/2024, 2:35 PMfun <Error, A, B> Raise<Nel<Error>>.mapFirstOrNull5(
iterable: Iterable<A>,
@BuilderInference transform: Raise<Error>.(A) -> B
): B? {
mapOrAccumulate(iterable) { return@mapFirstOrNull5 transform(it) }
return null
}
• if we get a correct value after transform
, the function returns early
• we get to return null
only if mapOrAccumulate
is not erroneous nor has returned early, which means the iterable must be nullChris Lee
01/22/2024, 2:43 PM@OptIn(ExperimentalTypeInference::class)
internal inline fun <Error, A, B> Raise<NonEmptyList<Error>>.mapFirstOrNull5(
iterable: Iterable<A>,
@BuilderInference transform: RaiseAccumulate<Error>.(A) -> B
): B? =
mapOrAccumulate<Error, A, B>(iterable) {
return@mapFirstOrNull5 transform(it)
}
.firstOrNull()
Alejandro Serrano.Mena
01/22/2024, 2:45 PMmap
using an early return, which is exactly what you would do using regular KotlinChris Lee
01/22/2024, 2:46 PMfirst {…}
etc; those aren’t available in conjuction with Arrow error accumulation.Alejandro Serrano.Mena
01/22/2024, 2:48 PMfirstOrAccumulate
...Youssef Shoaib [MOD]
01/22/2024, 2:48 PMforEachAccumulating
method would be desirable as a primitive? It would allow for such functions to be easily defined, and thus Arrow could have firstOrAccumulate
, lastOrAccumulate
etc easilyChris Lee
01/22/2024, 2:50 PMforEachAccumulating
is interesting. Would be nice if it had the flexibility to support various operations - first, last, takeWhile, etc.Alejandro Serrano.Mena
01/22/2024, 2:50 PMmapOrAccumulate
can work as such primitive, since forEachAccumulating
is just the version in which each element has a Unit
transformationChris Lee
01/22/2024, 2:50 PMmapOrAccumulate
processes the whole list, with limited means of short-circuiting - we hacked that for first
by returning, but that won’t easily work for say takeWhile
.Alejandro Serrano.Mena
01/22/2024, 2:51 PMforEachAccumulating
, you can write it using mapOrAccumulate
Chris Lee
01/22/2024, 2:53 PMforEachAccumulating
.Chris Lee
01/22/2024, 2:56 PMforEachAccumulating
- would/should it support criteria on the errors themselves?
atm stdlib provides list.takeWhile { … } to stop at a certain point - we can provide an Arrow version for the success path, but what if the criteria is something like “stop after 5 errors” or “stop after error x encountered” on the error path?Alejandro Serrano.Mena
01/22/2024, 2:57 PMRaise
is about working implicitly with the errors, in the case you mention I think you should rather use either
and build this logic yourselfAlejandro Serrano.Mena
01/22/2024, 2:58 PMChris Lee
01/22/2024, 3:05 PMfirstOrAccumulate
(formerly known as mapFirstOrNull5
); it loops through constructors on a class, trying each one in turn until one succeeds (or all fail).
return withError({ it.first() }) {
firstOrAccumulate(singleArgConstructors) { constructor ->
val args =
constructor.convertArguments(
mapOf(constructor.parameters[0] to sourceNode),
context,
ConversionError::ValueClassParameterErrors
)
catch({ constructor.callBy(args) }) { thrown ->
raise(ValueClassConstructorException(target.type, thrown))
}
}
} ?: raise(ConversionError.NoValueClassConstructor(valueClass))
Chris Lee
01/22/2024, 3:05 PM