Reputation: 43
I want to be able to polymorphically handle multiple Either
values who share the same Left
type, but not the same Right
type, e.g.:
foo = Left "foo" :: Either String Int
bar = Left "bar" :: Either String Char
list = [foo, bar]
The Left
value indicates an error message in case the computation failed, while the Right
value has different types depending on different computations. In case of failure I only care about gathering the errors on the left, so it would be useful to be able to treat all of them the same for this purpose.
However, this fails, and the compiler expects both type variables to match:
* Couldn't match type `Char' with `Int'
Expected type: Either String Int
Actual type: Either String Char
* In the expression: bar
In the expression: [foo, bar]
In an equation for `list': list = [foo, bar]
After a lot of searching, the only thing I stumbled upon is something called a "Partial Type Signature", which uses the "_" wildcard, and has to be enabled explicitly by a flag for it to compile. However, even after enabling it, it still doesn't seem to change anything:
foo = Left "foo" :: Either String Int
bar = Left "bar" :: Either String Char
list = [foo, bar] :: Either String _
And I receive the same error:
* Couldn't match type `Char' with `Int'
Expected type: Either String Int
Actual type: Either String Char
* In the expression: bar
In the expression: [foo, bar] :: Either String _
In an equation for `list': list = [foo, bar] :: Either String _
In Java (if it had an Either
type), I could simply do something like this:
List<Either<String, ?>> list = asList(foo, bar);
And it would have worked just fine, type safe and whatnot.
Is there a type variable wildcard in Haskell corresponding to ?
in Java? If not, how do you deal with such cases where you don't care about a certain type variable and want to abstract it? Is there an alternative?
More details:
Realizing that I wasn't clear enough about my problem, I will elaborate:
What I'm trying to implement is a command line parser. It should receive the user's input (command line arguments) and return a Params
value, where:
data Params = Params String Double Bool
For simplicity's sake I only included 3 values in the value constructor above, but actually there are many more.
Since there are many different parameters, each one can fail in its own way - the user might not provide the necessary argument, the provided argument is invalid, etc.
The parsing should succeed only if all of the arguments provided are valid. If any of them is invalid, then the entire computation should fail. So I thought about the following solution (again, it's simplified, so please ignore other issues like efficiency):
parse :: [String] -> Either [String] Params
parse args = if successful
then Right (Params strParam doubleParam boolParam)
else Left errors
where successful = null errors
(Right strParam) = eitherStr
(Right doubleParam) = eitherDouble
(Right boolParam) = eitherBool
errors = lefts [eitherStr, eitherDouble, eitherBool]
eitherStr = parseStr args
eitherDouble = parseDouble args
eitherBool = parseBool args
parseStr :: [String] -> Either String String
parseDouble :: [String] -> Either String Double
parseBool :: [String] -> Either String Bool
But it doesn't compile, since I can't handle eitherStr
, eitherDouble
and eitherBool
as having the same type, which would have been possible if there was a wildcard type variable. Hence my question.
Under the current circumstances, I have to resort to something like this:
parse :: [String] -> Either [String] Params
parse args = if successful
then Right (Params strParam doubleParam boolParam)
else Left errors
where successful = null errors
(Right strParam) = eitherStr
(Right doubleParam) = eitherDouble
(Right boolParam) = eitherBool
errors = concat [strErrors, doubleErrors, boolErrors]
strErrors = getErrors eitherStr
doubleErrors = getErrors eitherDouble
boolErrors = getErrors eitherBool
eitherStr = parseStr args
eitherDouble = parseDouble args
eitherBool = parseBool args
getErrors = lefts . (:[])
parseStr :: [String] -> Either String String
parseDouble :: [String] -> Either String Double
parseBool :: [String] -> Either String Bool
Doable, but messier. Of course, it's possible that my solution is completely off track, and there's a better idiomatic functional solution, which I'll be glad to hear about.
It was said that Java has Object
as a universal upper bound, so the comparison is misleading. But it seems to me that there can always be a theoretical upper bound, even if in the worst case that upper bound means a value which you can't use. Which is just fine for cases where you don't need it, such as mine.
After trying existential types:
Existential types are of no help to me. They define a new type that wraps the old one, so functions that accept Either
won't accept the new type, and can't be reused. So instead of using the existing lefts
function, I have to implement it myself for the new type, in addition to the hassle of defining a new type:
data SomeEither l = forall r. SomeEither (Either l r)
data Params = Params String Double Bool deriving (Show)
parse :: [String] -> Either [String] Params
parse args = if successful
then Right (Params strParam doubleParam boolParam)
else Left errors
where successful = null errors
(Right strParam) = eitherStr
(Right doubleParam) = eitherDouble
(Right boolParam) = eitherBool
errors = lefts' [SomeEither eitherStr, SomeEither eitherDouble, SomeEither eitherBool]
eitherStr = parseStr args
eitherDouble = parseDouble args
eitherBool = parseBool args
lefts' :: [SomeEither l] -> [l]
lefts' [] = []
lefts' ((SomeEither (Left l)):xs) = l:(lefts' xs)
lefts' (x:xs) = lefts' xs
parseStr :: [String] -> Either String String
parseDouble :: [String] -> Either String Double
parseBool :: [String] -> Either String Bool
The whole point was to do less work and have cleaner code. The use of existential types in this case does the opposite.
Upvotes: 4
Views: 1182
Reputation: 29193
This is fruitless. Either L R
where R
is unknown is just Maybe L
. If you have Right (r :: R)
and put it into your list, thereby forgetting what R
is, then even though you can later take that Right r
out of the list, you will not know the type of r
. That makes it unusable. The same is basically true in Java: a Either<L, ?>
is really just an Optional<L>
. If the Either
is a Right
, well, all you get is an Object
(the upper bound of the wildcard). Ignoring the "universal" Object
operations in Java (most importantly instanceof
and ==
, but also equals
, hashCode
, wait
, synchronized
, etc.), none of which exist in Haskell (because Haskell values lack identity and runtime type), there's nothing you can do with this featureless blob. Why keep it in the first place?
Now, as to syntax: _
wildcards in Haskell types are markers that you don't know what type goes there and you want GHC to infer it. I could write:
something :: [a] -> _
something = foldr (:) []
and GHC will tell me I should fill in the _
with [a]
, because that's the type of something
. The _
doesn't allow for anything "new"; it's just a convenience for when you don't know what the type of something is but you want to write it out for clarity (or when you don't want to write out the type because it's big, but that's rarer). In your case, [foo, bar]
is ill-typed, period. Lists must be homogenous (contain elements of the same type) and [foo, bar]
simply is not valid, and asking GHC to tell you what the type is not going to help you when there is no type in the first place.
The equivalent to Java's ?
types are existential types:
-- SomeEither l = Either<L, ?>
data SomeEither l = forall r. SomeEither (Either l r)
A SomeEither l
(declared on the LHS) value is (according to the RHS) a pair, consisting of a type r :: Type
and a value of type Either l r
. This is also the meaning of Either<L, ?>
in Java: Either<L, ?>
is the type of pairs consisting of a type R
(abusing the Java jargon, R
is the "capture type") and a value Either<L, R>
. However, in Java, the packing and unpacking of these pairs is implicit (packing is called "upcasting" and unpacking is called "wildcard capture"). In Haskell, you'd have to say
list = [SomeEither foo, SomeEither bar]
but now we're back to my first point that now you can never use the Right
s ever again:
forM_ list (\(SomeEither x) ->
case x of
Left l -> putStrLn l
Right r -> putStrLn ":(") -- don't know type of r; can't really use it for anything!
So, if you're going to have to preprocess the values before they go into the list, and if there's no point keeping the Right
values around, then just get rid of them at the beginning:
list :: [Maybe String]
list = [left foo, left bar]
where left = either Just (const Nothing)
-- or maybe you just want
list :: [String]
list = catMaybes [left foo, left bar]
where left = either Just (const Nothing)
EDIT: "Putting things in lists" is 100% not the solution to your edited question. Note that even if you had wildcard types like in Java, they wouldn't really help, either. I think you actually want this Applicative
(which is almost WriterT [String] Maybe
):
newtype Validated a = Validated { runValidated :: Either [String] a }
deriving Functor
instance Applicative Validated where
pure = Validated . Right
Validated l <*> Validated r = Validated (l <!> r)
where
Left xs <!> Left ys = Left (xs ++ ys)
Left xs <!> Right _ = Left xs
Right _ <!> Left xs = Left xs
Right f <!> Right xs = Right (f xs)
parseStr :: [String] -> Validated String
parseDouble :: [String] -> Validated Double
parseBool :: [String] -> Validated Bool
parse :: [String] -> Validated Params
parse args = Params <$> parseStr args <*> parseDouble args <*> parseBool args
(Sadly, it's not a Monad
.) No erasing of types, nothing fancy. Just combining all the errors and producing a value when there are none.
Upvotes: 7
Reputation: 233150
(Old answer below)
Based on the updated question, it sounds like a standard validation problem. This is essentially a FAQ, so I'll refer you to another answer of mine for more details.
You can use Applicative
to collect error messages, but for various reasons, the Either
instance isn't suitable. What you can do is wrap Either
in a newtype
with a more appropriate Applicative
instance and use that instead:
newtype Validation e r =
Validation { runValidation :: Either e r } deriving (Eq, Show, Functor)
instance Semigroup m => Applicative (Validation m) where
pure = Validation . pure
Validation (Left x) <*> Validation (Left y) = Validation (Left (x <> y))
Validation f <*> Validation r = Validation (f <*> r)
It'll also make things easier for you if you change the individual parsing functions to return Validation
values:
parseStr :: String -> Validation [String] String
parseDouble :: String -> Validation [String] Double
parseBool :: String -> Validation [String] Bool
But if you don't want to do that, you can always lift Either
-returning functions to Validation
afterwards.
Now you can easily compose the desired parse
function:
parse :: [String] -> Either [String] Params
parse [x, y, z] =
runValidation $ Params <$> parseStr x <*> parseDouble y <*> parseBool z
parse _ = Left ["wrong number of arguments."]
There are at least two reusable libraries that provide this functionality out of the box.
You can't put Either String Int
and Either String Char
values into the same list, because they don't have the same type. As chi suggests, you can wrap the Right
values in an existential type, or perhaps just eliminate them by mapping all values to Either String ()
.
If you're only looking for a way to collect all error messages, you can also use the lefts
function.
Let's expand the example a bit:
import Data.Either
foo = Left "foo" :: Either String Int
bar = Left "bar" :: Either String Char
baz = Left "baz" :: Either String Char
qux = Right 42 :: Either String Int
list1 :: [Either String Int]
list1 = [foo, qux]
list2 :: [Either String Char]
list2 = [bar, baz]
You can put foo
and qux
in the same list, because they have the same type. The same goes for bar
and baz
.
You can pull out all the Left
values of both lists and concatenate them:
errors :: [String]
errors = lefts list1 ++ lefts list2
Given the above values, errors
has the value ["foo","bar","baz"]
. The number 42
isn't included, because that was a Right
value.
I get the impression, however, that this might be an XY problem. Which problem are you actually trying to solve?
Upvotes: 2