哈斯克尔中是否有通配符类型变量



我希望能够多态地处理共享相同Left类型但不具有相同Right类型的多个Either值,例如:

foo = Left "foo" :: Either String Int
bar = Left "bar" :: Either String Char
list = [foo, bar]

Left值指示计算失败时的错误消息,而Right值根据不同的计算具有不同的类型。如果失败,我只关心收集左侧的错误,因此能够为此目的对所有错误一视同仁会很有用。

但是,这将失败,并且编译器希望两个类型变量都匹配:

* 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]

经过大量搜索,我唯一偶然发现的是称为"部分类型签名"的东西,它使用"_"通配符,并且必须通过标志显式启用才能编译。但是,即使启用它,它似乎仍然没有改变任何东西:

foo = Left "foo" :: Either String Int
bar = Left "bar" :: Either String Char
list = [foo, bar] :: Either String _

我收到同样的错误:

* 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 _

在Java中(如果它有一个Either类型),我可以简单地做这样的事情:

List<Either<String, ?>> list = asList(foo, bar);

它会工作得很好,键入安全等等。

Haskell中是否有与Java中的?对应的类型变量通配符?如果不是,你如何处理这种你不关心某个类型变量并想抽象它的情况?有没有其他选择?

更多详情:

意识到我对我的问题不够清楚,我将详细说明:

我正在尝试实现的是命令行解析器。它应该接收用户的输入(命令行参数)并返回一个Params值,其中:

data Params = Params String Double Bool

为了简单起见,我在上面的值构造函数中只包含 3 个值,但实际上还有更多。

由于有许多不同的参数,每个参数都可能以自己的方式失败 - 用户可能没有提供必要的参数,提供的参数无效等。

仅当提供的所有参数都有效时,分析才应成功。如果其中任何一个无效,则整个计算应该失败。所以我想到了以下解决方案(同样,它被简化了,所以请忽略效率等其他问题):

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

但它不能编译,因为我无法将eitherStreitherDoubleeitherBool处理为具有相同的类型,如果有通配符类型变量,这是可能的。因此,我的问题。

在目前的情况下,我不得不求助于这样的事情:

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

可行,但更混乱。当然,我的解决方案可能完全偏离了轨道,并且有一个更好的惯用函数解决方案,我很高兴听到。

有人说JavaObject是一个通用的上限,所以这种比较是误导性的。但在我看来,理论上总是可以有一个上限,即使在最坏的情况下,上限意味着你不能使用的值。这对于您不需要它的情况很好,例如我的。

尝试存在类型后:

存在主义类型对我没有帮助。它们定义了一个包装旧类型的新类型,因此接受Either的函数将不接受新类型,并且不能重用。因此,除了定义新类型的麻烦之外,我还必须为新类型自己实现它,而不是使用现有的lefts函数:

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

重点是减少工作量并拥有更干净的代码。在这种情况下,使用存在类型则相反。

这是徒劳的。Either L RR未知的地方只是Maybe L.如果您有Right (r :: R)并将其放入列表中,从而忘记了R是什么,那么即使您以后可以将该Right r从列表中删除,您也不会知道r的类型。这使得它无法使用。在Java中基本上也是如此:Either<L, ?>实际上只是一个Optional<L>。如果EitherRight,那么,你得到的只是一个Object(通配符的上限)。忽略Java中的"通用"Object操作(最重要的是instanceof==,还有equalshashCodewaitsynchronized等),这些操作在Haskell中都不存在(因为Haskell值缺少标识和运行时类型),你对这个无特征的blob无能为力。为什么要保留它?

现在,关于语法:Haskell类型中的_通配符是标记,您不知道哪种类型在那里,并且您希望GHC推断它。我可以写:

something :: [a] -> _
something = foldr (:) []

GHC会告诉我我应该用[a]填写_,因为这就是something的类型。_不允许任何"新"的东西;当您不知道某物的类型是什么但为了清楚起见而想写出来时(或者当您不想写出类型,因为它很大,但这种情况很少见)时,这只是一种方便。在您的情况下,[foo, bar]输入不正确,句号。列表必须是同质的(包含相同类型的元素),[foo, bar]根本无效,当首先没有类型时,要求 GHC 告诉您该类型对您没有帮助。

与Java的?类型等效的是存在类型:

-- SomeEither l = Either<L, ?>
data SomeEither l = forall r. SomeEither (Either l r)

SomeEither l(在 LHS 上声明)值是(根据 RHS)一对,由r :: Type型和Either l r型值组成。这也是 Java 中Either<L, ?>的含义:Either<L, ?>是由类型R(滥用 Java 术语,R是"捕获类型")和值Either<L, R>组成的对的类型。但是,在 Java 中,这些对的打包和解包是隐式的(打包称为"向上转换",解包称为"通配符捕获")。在哈斯克尔,你不得不说

list = [SomeEither foo, SomeEither bar]

但是现在我们回到我的第一点,现在你再也不能使用Right了:

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!

因此,如果您必须在值进入列表之前对其进行预处理,并且如果保留Right值没有意义,那么只需在开始时删除它们:

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)

编辑:"将内容放在列表中"不是100%不是您编辑的问题的解决方案。请注意,即使您有像 Java 中的通配符类型,它们也不会真正提供帮助。我认为您实际上想要这个Applicative(几乎是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

(可悲的是,这不是一个Monad。没有擦除类型,没有什么花哨的。只是将所有错误组合在一起,并在没有错误时产生一个值。

基于更新问题的新答案

(下面的旧答案)

根据更新的问题,这听起来像是一个标准的验证问题。这本质上是一个常见问题解答,所以我将向您推荐我的另一个答案以获取更多详细信息。

您可以使用Applicative来收集错误消息,但由于各种原因,Either实例不适合。您可以做的是将Either包装在具有更合适的Applicative实例的newtype中,并改用它:

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)

如果您更改各个解析函数以返回Validation值,也会让您更轻松:

parseStr :: String -> Validation [String] String
parseDouble :: String -> Validation [String] Double
parseBool :: String -> Validation [String] Bool

但是,如果您不想这样做,则可以随时将Either返回功能提升到Validation

现在,您可以轻松编写所需的parse函数:

parse :: [String] -> Either [String] Params
parse [x, y, z] =
runValidation $ Params <$> parseStr x <*> parseDouble y <*> parseBool z
parse _ = Left ["wrong number of arguments."]

至少有两个可重用的库提供开箱即用的此功能。

<小时 />

旧答案

不能将Either String Int值和Either String Char值放入同一列表中,因为它们的类型不同。正如chi所建议的那样,您可以将Right值包装为存在类型,或者只是通过将所有值映射到Either String ()来消除它们。

如果您只是在寻找一种收集所有错误消息的方法,您还可以使用lefts函数。

让我们稍微扩展一下示例:

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]

您可以将fooqux放在同一个列表中,因为它们具有相同的类型。barbaz也是如此.

您可以提取两个列表的所有Left值并连接它们:

errors :: [String]
errors = lefts list1 ++ lefts list2

给定上述值,errors的值为["foo","bar","baz"]。不包括数字42,因为这是一个Right值。

但是,我的印象是,这可能是XY问题。您实际上要解决哪个问题?

最新更新