Hi all.
Im trying to compose a structure of objects that can produce an error state.
I want the nature of the error to be easily discoverable to our developers, and extensible. So with each child object producing its own error type poly variant (with its open type variable), and including those as children of an outer poly variant (with its open type variable), I guess Im looking for a way to include the child type variables as elements of the parent somehow. Is this possible?
something like:
type a<'a> = ([> #One | #Two('b)| #Three('c)]] as 'a)
Why do your “sub”-error types need to be open? Actually, you could set it up so none of your error types themselves are open…rather make the result type returned by your functions open instead. Something like this: playground
module Foo = {
type error = [#SillyError | #YuckyError]
let run: int => result<int, [> error]> = _ => failwith("TODO")
}
module Bar = {
type error = [Foo.error | #NegativeArgError]
let run: int => result<int, [> error]> = n => {
if n < 0 {
Error(#NegativeArgError)
} else {
Foo.run(n)
}
}
}
module Baz = {
type error = [#WildError | #CrazyError]
let run: int => result<int, [> error]> = _ => failwith("TODO")
}
module App = {
type error = [Foo.error | Bar.error | Baz.error]
// Eventually, you probably want to close the error type so you can ensure you don't
// produce garbage polyvariants at some point by mistake.
let run: int => result<int, error> = n => {
let flatMap = Belt.Result.flatMap
Foo.run(n)->flatMap(Bar.run)->flatMap(Baz.run)
}
}
The errors compose just fine, you can see at a glance the error variants each individual function can return, and eventually, once you’re done you can specify a closed polymorphic variant type when you need to so you can prevent yourself from making a mistake with all this open-poly stuff. (E.g., putting a type like #CrazyEror instead of #CrazyError.)
Sure, the example I gave does that. Here it is tweaked slightly so that no function (other than your toplevel app function) knows about any other functions error variants.
Eventually some function will have to know about the error variants other functions can produce though if you want to handle them. And if you don’t care about handling them in a specific way depending on the specific error variant, then you shouldn’t be using variants for the payload at all, use some string or other unified error value and call it a day.
I am composing modules with functors to build form structures, so the error types you have above become opaque when passed through the module type of the functor argument.
Would you mind putting an example of what you’re trying to do, I’m pretty curious now.
Just because you are running things through a functor doesn’t mean that the error types need to be opaque. E.g., this composes the errors in the exact same way.
Edit: I must be misunderstanding what you are trying to do. Is there a reason you need the error type to also be abstract?