CLOVIS
04/23/2024, 3:52 PMEither
variant that supports unfinished operations using a three-state sealed class. How could interoperability with the Raise
DSL look like?simon.vergauwen
04/23/2024, 3:54 PMsimon.vergauwen
04/23/2024, 3:56 PMRaise
per failure subcase.
inline fun <E, A> outcome(block: (Raise<Incomplete>, Raise<Failure<E>>) () -> A): ProgressiveOutcome<E, A> = ...
simon.vergauwen
04/23/2024, 3:56 PMIncomplete
and Failure
and put that into Raise<E>
Youssef Shoaib [MOD]
04/23/2024, 3:57 PMRaise<Error>
, and a Raise<Progress>
with contexts
Dang, Simon beat me to it, although I'm suggesting to make sure the type is unwrappedCLOVIS
04/23/2024, 3:57 PMCLOVIS
04/23/2024, 3:58 PMProgressiveOutcome.Success
can be in-progresssimon.vergauwen
04/23/2024, 3:58 PMI see, so the Quiver team considers that any non-finished value is a raise-able failureUhm, no 🤔 I wrote that implementation, but did a dirty trick to allow naturally binding
Either<E, A>
. It's a pretty neat trick 😄
Allow raising Any?
, and have the public API nicely typed.simon.vergauwen
04/23/2024, 3:59 PMCLOVIS
04/23/2024, 3:59 PMCLOVIS
04/23/2024, 3:59 PMsimon.vergauwen
04/23/2024, 4:05 PMsimon.vergauwen
04/23/2024, 4:05 PMEither<E, A>.bind()
on Left
should now show-up as ProgressiveOutcome.Failure
which is pretty coolYoussef Shoaib [MOD]
04/23/2024, 4:12 PMFailure
. The code also doesn't handle Incomplete
on the recover side which I'm guessing was a simple oversight, but it kinda shows how easily the code can do unexpected things.
Perhaps what would be better is 3 context receivers:
Raise<Progress>
for incomplete, Raise<Pair<E, Progress>>
for Failure, and a Raise<E>
, which is just the failure raise .withError({ it to done() }) { ... }
simon.vergauwen
04/23/2024, 4:16 PMsimon.vergauwen
04/23/2024, 4:35 PMprogress
state to OutcomeRaise
.
val progress: Int
fun updateProgress(block: (Int) -> Int): Unit
simon.vergauwen
04/23/2024, 4:36 PMdone
by default. Some strategy can even be applied here, which is accepted as param in outcome { }
.simon.vergauwen
04/23/2024, 4:37 PMI can see issues here if e.g. someone tries to raise aCan you give an example?. The code also doesn't handleFailure
on the recover side which I'm guessing was a simple oversight, but it kinda shows how easily the code can do unexpected things.Incomplete
Youssef Shoaib [MOD]
04/23/2024, 4:43 PMIncomplete(42).bind()
results in a Failure(Incomplete(42), Int.MAX_VALUE)
Secondly, with a OutcomeRaise<Outcome<F, S>>
, calling raise(myFailure)
where myFailure: Failure<F>
will fail with a CCE when later examined because it'll "flatten out" the failure i.e. it'll result in myFailure
when the intention was Failure(myFailure, Int.MAX_VALUE)
simon.vergauwen
04/23/2024, 4:44 PMCLOVIS
04/23/2024, 4:45 PMCLOVIS
04/23/2024, 4:47 PMraise(5)
🤔Youssef Shoaib [MOD]
04/23/2024, 4:48 PMraise(myFailure)
can be fixed without a bunch of code gymnastics. In fact, now I realise that this is an issue with Quiver too. Inside of an OutcomeRaise<Outcome<...>>
, doing raise(Absent)
will result in an output of Absent
instead of Failure(Absent)
simon.vergauwen
04/23/2024, 4:48 PMsimon.vergauwen
04/23/2024, 4:48 PMsimon.vergauwen
04/23/2024, 4:48 PMsimon.vergauwen
04/23/2024, 4:48 PMfun main() {
val x = outcome<String, Int> {
ProgressiveOutcome.Success(1).bind() + ProgressiveOutcome.Success(1).bind()
// ProgressiveOutcome.Incomplete(41).bind()
// "Failure".left().bind()
ProgressiveOutcome.Failure("Failure").bind()
}
println(x)
}
This all works as expectedsimon.vergauwen
04/23/2024, 4:49 PMraise("Failure")
simon.vergauwen
04/23/2024, 4:49 PMFailure
, but Incomplete
works fine for me.simon.vergauwen
04/23/2024, 4:50 PMsimon.vergauwen
04/23/2024, 4:50 PMraise(ProgressiveOutcome.Failure("Failure"))
rather than bind, right?Youssef Shoaib [MOD]
04/23/2024, 4:50 PMCLOVIS
04/23/2024, 4:50 PMraise
overloadsimon.vergauwen
04/23/2024, 4:51 PMRaise
is fixed to E
, not ProgressiveOutcome
simon.vergauwen
04/23/2024, 4:51 PMsimon.vergauwen
04/23/2024, 4:51 PMfailure(progress: Progress, e: E)
simon.vergauwen
04/23/2024, 4:52 PMRaise<E>.() -> A
based program inside your DSLYoussef Shoaib [MOD]
04/23/2024, 4:59 PMfun main() {
fun consumeProgressiveOutcome(outcome: ProgressiveOutcome<String, Unit>) { }
val x = outcome<ProgressiveOutcome<String, Unit>, Int> {
raise(ProgressiveOutcome.Failure("error"))
}
when (x) {
is ProgressiveOutcome.Failure -> {
consumeProgressiveOutcome(x.failure)
}
else -> {}
}
}
The way around this would be to use custom wrapper types inside OutcomeRaise
, and then unwrap inside outcome
, while insuring that those custom wrappers never escape to the outside world. The issue here is that we use Failure
and Incomplete
as signalling values internally, so sometimes we might mistaken legitimate values as signalsCLOVIS
04/23/2024, 5:01 PMRaise<E>.() -> A
, isn't this simpler?
@JvmInline
value class ProgressiveOutcomeDsl<Failure>(private val raise: Raise<ProgressiveOutcome.Unsuccessful<Failure>>) : Raise<Failure> {
override fun raise(r: Failure): Nothing =
raise(r, done())
@JsName("raiseUnsuccessful")
fun raise(failure: ProgressiveOutcome.Unsuccessful<Failure>): Nothing =
raise.raise(failure)
@JsName("raiseWithProgress")
fun raise(failure: Failure, progress: Progress = done()): Nothing =
raise(ProgressiveOutcome.Failure(failure, progress))
// …bind and stuff
}
simon.vergauwen
04/23/2024, 5:02 PMsimon.vergauwen
04/23/2024, 5:03 PMYoussef Shoaib [MOD]
04/23/2024, 5:05 PMCLOVIS
04/23/2024, 5:06 PMYoussef Shoaib [MOD]
04/23/2024, 5:14 PMRaise
functions. E.g. if someone calls recover
and inside calls raise(failure, progress)
, the raised value won't be caught by recover. If you're going all-in on contexts, then I'd suggest instead using context(Raise<ProgressiveOutcome.Unsuccessful<Failure>>, Raise<Failure>)
, and then exposing your raiseUnsuccessful
and raiseWithProgress
as extension funs. This way, you get support for recover
et al. You wouldn't even need your own value class
thenCLOVIS
04/23/2024, 5:16 PMEverything is wrapped in Failure, but looking at it, it's actually not unnecessary.Is it not? The return type of the DSL is
ProgressiveOutcome<Failure, Value>
anyway, so the wrapping will happen by that point anyway, no?CLOVIS
04/23/2024, 5:17 PMIf you're going all-in on contexts,…I am, but I support multiple platforms, so although I can start to design with them in my mind, I can't use them 😕
CLOVIS
04/23/2024, 5:17 PME.g. if someone callsI don't understand this situation, do you have an example?and inside callsrecover
, the raised value won't be caught by recover.raise(failure, progress)
Youssef Shoaib [MOD]
04/23/2024, 5:18 PMwithError
calls in a contexts worldYoussef Shoaib [MOD]
04/23/2024, 5:21 PMoutcome<String, Int> {
recover({
raise("String", Progress.blah(42))
}) { 1 }
}
This should return Success(1)
ideally, but here I think it either results in a compiler error because of unspecified type params, or it results in Failure("String", Progress.blah(42))
.
However, if `outcome`'s block
has context(Raise<Failure>, Raise<ProgressiveOutcome.Unsuccessful<Failure>>)
and your functions are defined as extensions on Raise<ProgressiveOutcome.Unsuccessful<Failure>>
, then everything works as expected and you get 1.simon.vergauwen
04/23/2024, 5:23 PMIor
, etcYoussef Shoaib [MOD]
04/23/2024, 5:27 PMfold
or any other Raise builders, the issue reappears. The real solution (at least as far as I've prototyped) is to do away with the wrapper Raise classes and instead use contexts. For instance, IorRaise<E>
can be decomposed into context(Raise<E>, IorAccumulate<E>)
where IorAccumulate
deals with the accumulation logic, and Raise
does its thing. With such decomposition, everything works as expected because recover
simply replaces the Raise
part without touching the IorAccumulate
.CLOVIS
04/23/2024, 5:28 PM@RaiseDsl
stop that from happening? It's a @DslMarker
, right?simon.vergauwen
04/23/2024, 5:28 PMsimon.vergauwen
04/23/2024, 5:29 PMCLOVIS
04/23/2024, 5:29 PMsimon.vergauwen
04/23/2024, 5:30 PMCLOVIS
04/23/2024, 5:30 PMYoussef Shoaib [MOD]
04/23/2024, 5:32 PM@RaiseDsl
doesn't actually mark Raise
, and that's on purpose. either<String, _> { either<Int, _> { raise(42) } }
is expected to work.
Hopefully once K2 is out fully, JB will give contexts more love.CLOVIS
04/23/2024, 5:33 PMsimon.vergauwen
04/23/2024, 5:34 PMflatMapLeft
is not possible to implementYoussef Shoaib [MOD]
04/23/2024, 5:38 PM@DslMarker
would forbid such usages implicitly, one can always use an explicit receiver. I'm also not sure whether context(Raise<A>, Raise<B>)
would be forbidden by @DslMarker
, but if it's forbidden, it'd be a huge nerf, and if it isn't forbidden, then we get to the same issue that we have here with recover
etcCLOVIS
04/23/2024, 5:53 PMYoussef Shoaib [MOD]
04/23/2024, 5:56 PMDslMarker
had a way to only get mad based on type parameter value, then perhaps it would be applicable here, but alas.Alejandro Serrano.Mena
04/23/2024, 6:53 PMYoussef Shoaib [MOD]
04/23/2024, 6:55 PMCLOVIS
04/23/2024, 8:06 PMOutcome
class, which doesn't have progress informationCLOVIS
04/23/2024, 8:08 PMRaise<Lce<Failure, Nothing>>
, which seems a bit strange to me. I won't be able to run raise programs over Raise<Failure>
.Youssef Shoaib [MOD]
04/23/2024, 8:11 PMProgressiveOutcome.Unsuccessful
(Aw man I just realised how cool this could be if we had Union types and perhaps a way to remove an element from a Union, then we could do ProgressiveOutcome - ProgressiveOutcome.Successful
and perhaps make an easy DSL super-function that does exactly what those docs say to do)CLOVIS
04/23/2024, 8:11 PMNot enough information to infer type variable Failure
on both the DSL and recover
😕CLOVIS
04/23/2024, 8:12 PMIncomplete | Failure<F>
🙂Youssef Shoaib [MOD]
04/23/2024, 8:13 PMrecover<ProgressiveOutcome.Unsuccessful<...>, _>
should hopefully do the trick, then the outer one will need outcome<Nothing, _>
until your example gets more complicatedCLOVIS
04/23/2024, 8:18 PMraise
is for the outer scope. I don't like this at all.Youssef Shoaib [MOD]
04/23/2024, 8:20 PMProgressiveOutcomeDsl.recover
functionCLOVIS
04/23/2024, 8:20 PMCLOVIS
04/23/2024, 8:20 PMCLOVIS
04/23/2024, 8:22 PMCLOVIS
04/23/2024, 8:23 PMdefine aI'm going to end up the entirety of Arrow within that one class thoughfunctionProgressiveOutcomeDsl.recover
CLOVIS
04/23/2024, 8:28 PMYoussef Shoaib [MOD]
04/23/2024, 8:29 PMrecover
, hence why that's the bandaid we went for.
One annoying option is to forego a custom DSL class entirely, and instead use Raise directly, and define all your methods as extensions on Raise<ProgressiveOutcome.Unsuccessful>
, even including bind
yes which is a shameYoussef Shoaib [MOD]
04/23/2024, 8:30 PMbind
callsiteCLOVIS
04/23/2024, 8:30 PMCLOVIS
04/23/2024, 8:31 PMYoussef Shoaib [MOD]
04/23/2024, 8:34 PMnullable
, option
, etc hence why it was crucial to keep around (and well because this issue only surfaced later). If you can live with a bind(foo)
call, then that's the way to go.
Btw, I know your library is multiplatform, but you can still provide nice syntax in the JVM module, so that could be a better trade-off