arrow-kt / arrow

Λrrow - Functional companion to Kotlin's Standard Library
http://arrow-kt.io
Other
6.13k stars 442 forks source link

Raise accumulating/zip DSL #3436

Open serras opened 3 months ago

serras commented 3 months ago

This PR introduces a mini-DSLs for "zip" operations in Raise. The idea is to extend zipOrAccumulate to any amount of arguments, and make the code a bit more linear, by using the power of property delegation.

These are two tests that showcase the functionality: you use accumulating to mark those parts which should "add" to the errors, instead of merely using fail-first, and using delegation you don't need to remember anything else. If you use a raise outside an accumulating, all the errors accumulated until that moment are added to the other one.

@Test fun raiseAccumulatingTwoFailures() {
  eitherNel {
    val x by accumulating { raise("hello") ; 1 }
    val y by accumulating { raise("bye") ; 2 }
    x + y
  } shouldBe nonEmptyListOf("hello", "bye").left()
}

@Test fun raiseAccumulatingIntermediateRaise() {
  eitherNel {
    val x by accumulating { raise("hello") ; 1 }
    raise("hi")
    val y by accumulating { 2 }
    x + y
  } shouldBe nonEmptyListOf("hello", "hi").left()
}
github-actions[bot] commented 3 months ago

Kover Report

File Coverage [67.30%]
arrow-libs/core/arrow-core-high-arity/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt 65.07%
arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Raise.kt 95.65%
arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt 65.07%
Total Project Coverage 60.82%
serras commented 3 months ago

I've tried a few new names. I've also realized that every "runner" (either, result, ...) can give raise to a new accumulate, so I made that part generic. The question now is... what is the relation between this new form of accumulation and RaiseAccumulate?

nomisRev commented 3 months ago

I am willing to sink time into the IntelliJ plugin to provide a project wide refactor if you can help me get running. I am willing to do that for every breaking change we make between the last patch and 2.0.0!

So please review all APIs again in whatever way you think makes sense. If you see a good opportunity to split things of to smaller modules, that are like extremely tiny and make sense like AutoClose/ResourceScope/SagaScope which is a single type Scope which I pitched like 2 years ago. I am going to take another stab at that now. It's 3 the same thing, a finaliser typeclass.

EDIT: couldn't sleep, wrote some silly code. Closed it, will do it properly after resting 😅

nomisRev commented 3 months ago

After thinking about this a bit more, the delegation is what forces to first split into a declaration, right? To prevent:

Person(
   accumulating { ensureNotNull(name) { "Null name" } },
   accumulating { ensure(age => 18) { "Not an adult" } },
)

That is not possible with proper accumulation, right? So, the following test case.

@Test fun raiseAccumulatingTwoFailures() {
  accumulate {
    val x by accumulating { raise("hello") ; 1 }
    val y by accumulating { raise("bye") ; 2 }
    val xy = x + y
    val z by accumulating { raise("BOOM"); 3 }
    xy + z
  } shouldBe nonEmptyListOf("hello", "bye").left()
}
serras commented 3 months ago

About accumulation, yes, the delegated property trick is what makes it "wait" until the moment the value is actually needed for the next step, while accumulating the rest.

In the last commit I've merged RaiseAccumulate with this new idea, and I think it gives a wonderful DSL for error accumulation. Many of the current operations become now combinations of accumulate, accumulating, and bindOrAccumulate.

CLOVIS-AI commented 3 months ago

This looks very nice! I played around on my own trying to get something like this to work, but I couldn't get anything I was happy with. It's great that we're finally going to be avoid multi-lambda syntax and the n-arity duplications.