【发布时间】:2020-10-05 00:04:20
【问题描述】:
我希望能够多态地处理多个Either 值,这些值共享相同的Left 类型,但不同的Right 类型,例如:
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
但它无法编译,因为我无法将 eitherStr、eitherDouble 和 eitherBool 视为具有相同类型,如果存在通配符类型变量,这将是可能的。因此我的问题。
在目前的情况下,我不得不求助于这样的事情:
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
可行,但更麻烦。当然,我的解决方案可能完全偏离轨道,并且有更好的惯用函数解决方案,我很高兴听到。
据说Java有Object作为通用上限,所以这种比较有误导性。但在我看来,总有一个理论上的上限,即使在最坏的情况下,上限意味着一个你不能使用的值。这对于您不需要它的情况(例如我的情况)来说很好。
尝试存在类型后:
存在类型对我没有帮助。他们定义了一种包装旧类型的新类型,因此接受 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
重点是减少工作并拥有更简洁的代码。在这种情况下使用存在类型则相反。
【问题讨论】:
-
你可能需要一个存在类型来包装你的
Eithers,然后写[MyExType]。它不像 Java 那样方便,因为我们没有子类型,因此必须进行一些包装。 -
Java 示例具有误导性。您可以使用通配符声明泛型类型,但创建此类类型的值始终使用特定类型作为绑定。这里编译器推断
List<Either<String, Object>>,因为Object 是最小的上限。使这成为可能的是通用超类型(以及一般的子类型)的存在,而不是通配符类型的存在。 -
万一失败,你只关心收集左边的
Strings。当您遇到右侧不同类型的异构列表时,您打算在成功的情况下做什么?我认为答案将决定最佳解决方案。 -
@amalloy 不,那是错误的。 Java 通配符绑定紧密。
List<Either<String, ?>>在伪 Haskell 中表示[exists r. Either String r]。foo、bar中的每一个都向上转换为Either<String, ?>,这是一种“正常”类型,然后被卡在列表中。您的意思是exists r. [Either String r],这在Java 中不能直接表达。现在,由于Either是(道德上)协变的,区别几乎不存在,但是例如对于List<UnaryOperator<?>>,您的解释意味着您可以将所有元素链接在一起,但实际上每个元素都可以有不同的类型。 -
根据您更新的问题,您肯定想要
Validation。类似于Either,但会累积错误而不是获取第一个错误,这正是针对此类问题的。
标签: haskell