Hi all! I almost sure I used to succeed with polymorphic variants to pass errors in long processing chains and even read an article (on ReasonML) which compares using regular variants, strings, and polyvariants for the second type argument of result
. The polyvariants were clear winners because they are flexible, composable, and type safe.
But now I stumble upon some kind of basic misunderstanding that results in compile errors:
let makeStageA = (x): result<int, [ | #ProblemA1 | #ProblemA2 ]> => {
switch x {
| 1 => Error(#ProblemA1)
| 2 => Error(#ProblemA2)
| x => Ok(x)
}
}
let makeStageB = (x): result<int, [ | #ProblemB1 | #ProblemB2 ]> => {
switch x {
| 1 => Error(#ProblemB1)
| 2 => Error(#ProblemB2)
| x => Ok(x)
}
}
let makeStageC = (x): result<int, [ | #ProblemC1 | #ProblemC2 ]> => {
switch x {
| 1 => Error(#ProblemC1)
| 2 => Error(#ProblemC2)
| x => Ok(x)
}
}
let make = x => {
let result = x->makeStageA->Result.flatMap(makeStageB)->Result.flatMap(makeStageC)
// The brute force works but does not scale well with more variants and deepness
/*let result = switch x->makeStageA {
| Ok(x') =>
switch x'->makeStageB {
| Ok(x'') =>
switch x''->makeStageC {
| Ok(_) as ok => ok
| Error(#ProblemC1) as err => err
| Error(#ProblemC2) as err => err
}
| Error(#ProblemB1) as err => err
| Error(#ProblemB2) as err => err
}
| Error(#ProblemA1) as err => err
| Error(#ProblemA2) as err => err
}*/
switch result {
| Ok(_x) => Js.log("Ok")
| Error(#ProblemB1) => Js.log("Solve problem with foo")
| Error(#ProblemA2) => Js.log("Solve problem with bar")
| Error(_p) => Js.log("Do something other")
}
}
Output:
[rescript] 25 │
[rescript] 26 │ let make = x => {
[rescript] 27 │ let result = x->makeStageA->Result.flatMap(makeStageB)->Result.flatM
[rescript] ap(makeStageC)
[rescript] 28 │
[rescript] 29 │ // The brute force works but does not scale well with more variants
[rescript] and deepness
[rescript]
[rescript] This has type: int => result<int, [#ProblemB1 | #ProblemB2]>
[rescript] Somewhere wanted: int => Belt.Result.t<int, [#ProblemA1 | #ProblemA2]>
[rescript]
[rescript] The incompatible parts:
[rescript] result<int, [#ProblemB1 | #ProblemB2]> (defined as
[rescript] Belt_Result.t<int, [#ProblemB1 | #ProblemB2]>) vs
[rescript] Belt.Result.t<int, [#ProblemA1 | #ProblemA2]> (defined as
[rescript] Belt_Result.t<int, [#ProblemA1 | #ProblemA2]>)
[rescript] These two variant types have no intersection
So, ReScript sees that [#ProblemA1 | #ProblemA2]
and [#ProblemB1 | #ProblemB2]
are incompatible. How can I explain it so that it would union the types to [> #ProblemA1 | #ProblemA2 | #ProblemB1 | #ProblemB2]
without bulky enumeration of every single possible case?
Bonus obstacle. I’d like not to touch the code/signatures of the makeStage*
functions. Imagine they are coming from 3-rd party libraries I have no control on. And actually, their signatures are nice and clear: they indicate they return no more than these specific errors.