Either forces you to use one
Either is defined (and often used) like so:
data Either errorType valueType = Left errorType | Right valueType
However, it's not extensible. The
errorType must always be the same. This can be annoying when one is using
Either in a monadic way:
foo :: Either SameErrorType foo = do a <- stringOrFailWithErrorType1 b <- stringOrFailWithErrorType2 -- uh-oh! Compiler error! pure $ doSomethingWith a b
One way around this is to use a nested
Either type (e.g. `Either3), but this comes at the cost of extra layers of "boxing:"
import Data.Either (Either(..)) import Data.Either.Nested (Either5) import Data.Either.Inject (inj) -- Syntactic sugar to help here... foo :: Either5 a b c d e foo = inj d -- but it comes down to this bar :: Either a (Either b (Either c (Either d e))) bar = Right (Right (Right (Left d)))
One could reduce the amount of boxing by using
Variant does not have a
Monad instance, so one cannot use the "do notation" in the original example above.
This library enables one to use all the type classes of
Ord1, which haven't yet been implemented) but with the extensible error type capaility of
How? By hard-coding one of the rows in
Variant to work like
Right and leaving the others to work like
What if you took
Variant and provided a newtype around it that hard-coded one of the rows to something that functions like
Right constructor (e.g.
("_" :: a)) and then made it extensible, so that the other rows function like an extensible
Left constructor (e.g.
("_" :: a | errorRows))? You get
Variant-based version of
newtype Veither errorRows a = Veither (Variant ("_" :: a | errorRows)) -- -- `=~=` means 'isomorphic to' -- -- Veither () a =~= a -- Veither (foo :: Int) a =~= Either Int a -- Veither (foo :: Int, bar :: String) a =~= Either Int (Either String a)
Now we can write our monadic
Either code without the 'same error type' restriction:
type PossibleErrors = (a :: ErrorType1, b :: ErrorType2) foo :: Veither PossibleErrors String foo = do a <- stringOrFailWithErrorType1 b <- stringOrFailWithErrorType2 -- not a problem! pure $ doSomethingWith a b main :: Effect Unit main = do -- if you would use `fromRight` (or some other -- `Either` function) for `Either`, -- add a 'v' in front of it and you have the -- corresponding function name for `Veither` log $ vfromRight handleFailure foo where handleFailure :: Variant PossibleErrors -> String handleFailure = case_ # on _a (const "error type 1 happened") # on _b (const "error type 2 happened")
Why use "_" as the label of
While it likely doesn't do much, I thought it would lessen the impact on the runtime checking since checking whether this Variant's type matches another occurs frequently. I also didn't want to restrict the possible names one could use in the error types.