【问题标题】:Is there a wildcard type variable in Haskell?Haskell 中是否有通配符类型变量?
【发布时间】: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

但它无法编译,因为我无法将 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

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

据说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&lt;Either&lt;String, Object&gt;&gt;,因为Object 是最小的上限。使这成为可能的是通用超类型(以及一般的子类型)的存在,而不是通配符类型的存在。
  • 万一失败,你只关心收集左边的Strings。当您遇到右侧不同类型的异构列表时,您打算在成功的情况下做什么?我认为答案将决定最佳解决方案。
  • @amalloy 不,那是错误的。 Java 通配符绑定紧密。 List&lt;Either&lt;String, ?&gt;&gt; 在伪 Haskell 中表示 [exists r. Either String r]foobar 中的每一个都向上转换为 Either&lt;String, ?&gt;,这是一种“正常”类型,然后被卡在列表中。您的意思是exists r. [Either String r],这在Java 中不能直接表达。现在,由于Either 是(道德上)协变的,区别几乎不存在,但是例如对于List&lt;UnaryOperator&lt;?&gt;&gt;,您的解释意味着您可以将所有元素链接在一起,但实际上每个元素都可以有不同的类型。
  • 根据您更新的问题,您肯定想要Validation。类似于Either,但会累积错误而不是获取第一个错误,这正是针对此类问题的。

标签: haskell


【解决方案1】:

这是徒劳的。 Either L R 其中R 是未知的只是Maybe L。如果您有Right (r :: R) 并将其放入您的列表中,从而忘记了R 是什么,那么即使您稍后可以将Right r 从列表中取出,您也不会知道r 的类型。这使它无法使用。在 Java 中基本上也是如此:Either&lt;L, ?&gt; 实际上只是 Optional&lt;L&gt;。如果Either Right,那么你得到的只是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&lt;L, ?&gt;的含义:Either&lt;L, ?&gt;是由一个类型R(滥用Java术语,R是“捕获类型”)和一个值Either&lt;L, R&gt;组成的对的类型。但是,在 Java 中,这些对的打包和解包是隐式的(打包称为“向上转换”,解包称为“通配符捕获”)。在 Haskell 中,你不得不说

list = [SomeEither foo, SomeEither bar]

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

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。)没有擦除类型,没有什么花哨的。只需组合所有错误并在没有错误时生成一个值。

【讨论】:

  • 感谢您的回答。我更新了我的问题以更好地反映我的问题,这可能还不够清楚。我不同意你关于“徒劳无功”的观点。 Either L R 其中R 是未知的不等于Maybe L,因为Either 具有未知的正确值(如果支持这种抽象)我可以直接使用接受Either 的函数,而对于Maybe,我必须按照您的建议先转换它。您可以在我在问题中提供的示例中看到,这会导致代码更加冗长和重复。存在类型也是如此。
  • @Whitetip 给定的Applicative 是否满足您的需求?
  • 您的Applicative 解决方案非常有趣,但是即使我使用-XDeriveFunctor 标志,它也无法为我编译。你能解释一下这是如何工作的吗?
  • @Whitetip 糟糕,已修复。一旦你这样做,它实际上与其他答案相同。
  • 谢谢。 :) Applicative 是我最好的新朋友。
【解决方案2】:

基于更新问题的新答案

(下面的旧答案)

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

您可以使用Applicative 收集错误消息,但由于各种原因,Either 实例不适合。你可以做的是用更合适的Applicative 实例将Either 包装在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-returning 函数提升到 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."]

at least two reusable libraries 提供开箱即用的此功能。


旧答案

您不能将 Either String IntEither 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 problem。您实际上想解决哪个问题?

【讨论】:

  • 感谢您的回答。我更新了我的问题以包含有关我的实际问题的更多详细信息。如您所见,我的观点是使用一个列表而不是多个列表。我知道这在 Haskell 中根本不可能做到。
  • 谢谢马克,虽然它确实需要启用一些标志才能编译。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-07-20
  • 2011-12-15
  • 2017-04-15
  • 1970-01-01
  • 2015-08-22
  • 1970-01-01
  • 2016-05-13
相关资源
最近更新 更多