类型类约束、多态性和 cassava-conduit

Typeclass constraints, polymorphism and cassava-conduit

在玩 Haskell 和 conduit 时,我遇到了一个我很难解释的行为。首先让我列出所有需要加载的模块和语言扩展来重现我的问题:

{-# LANGUAGE FlexibleContexts  #-}

import Conduit                         -- conduit-combinators
import Data.Csv                        -- cassava
import Data.Csv.Conduit                -- cassava-conduit
import qualified Data.ByteString as BS -- bytestring
import Data.Text (Text)                -- text
import Control.Monad.Except            -- mtl
import Data.Foldable

首先我创建了最通用的 CSV 解析管道:

pipeline :: (MonadError CsvParseError m, FromRecord a)
         => ConduitM BS.ByteString a m ()
pipeline = fromCsv defaultDecodeOptions NoHeader

然后,我想输出我的 csv 文件每一行中的元素数量 - 我知道这有点愚蠢且无用,而且还有十亿种其他方法可以做这种事情,但那是只是一个玩具测试。

所以我打开 GHCi 并尝试了这个:

ghci> :t pipeline .| mapC length

正如预期的那样,这没有起作用,因为约束 FromRecord a 不能保证 aFoldable。所以我定义了以下管道:

pipeline2 :: (MonadError CsvParseError m, FromField a)
          => ConduitM BS.ByteString [a] m ()
pipeline2 = fromCsv defaultDecodeOptions NoHeader

这是一个合法的定义,因为根据 cassava 文档,FromField a => FromField [a]FromRecord 的一个实例。

在这一点上,我感到高兴和充满希望,因为 []Foldable 的一个实例。所以,我再次打开 GHCi,然后尝试:

ghci> :t pipeline2 .| mapC length

但我得到:

<interactive>:1:1: error:
    • Could not deduce (FromField a0) arising from a use of ‘pipeline2’
      from the context: MonadError CsvParseError m
        bound by the inferred type of
                 it :: MonadError CsvParseError m => ConduitM BS.ByteString Int m ()
        at <interactive>:1:1
      The type variable ‘a0’ is ambiguous
      These potential instances exist:
        instance FromField a => FromField (Either Field a)
          -- Defined in ‘cassava-0.4.5.0:Data.Csv.Conversion’
        instance FromField BS.ByteString
          -- Defined in ‘cassava-0.4.5.0:Data.Csv.Conversion’
        instance FromField Integer
          -- Defined in ‘cassava-0.4.5.0:Data.Csv.Conversion’
        ...plus 9 others
        ...plus 11 instances involving out-of-scope types
        (use -fprint-potential-instances to see them all)
    • In the first argument of ‘(.|)’, namely ‘pipeline2’
      In the expression: pipeline2 .| mapC length

所以我的理解是我的pipeline2不够详细

但现在,如果我尝试打造一个具有(几乎)相同类型的普通管道:

pipeline3 :: (MonadError CsvParseError m, FromField a)
          => ConduitM a [a] m ()
pipeline3 = awaitForever $ \x -> yield [x]

我再次打开 GHCi 并尝试:

ghci> :t pipeline3 .| mapC length

这次我得到:

pipeline3 .| mapC length
  :: (FromField a, MonadError CsvParseError m) => ConduitM a Int m ()

所以这一次,GHCi 明白我不必进一步指定 pipeline3 的定义。

所以我的问题是:为什么 pipeline2 有问题?有没有一种方法可以定义最通用的 "pipeline" 而无需进一步指定管道输出的类型? 我认为 FromField 个对象的列表就足够了。

感觉我错过了关于类型类以及如何以多态方式组合函数或这里的 Conduit 对象的重要观点。

非常感谢您的回答!

pipeline3 是一个类型类似于 ConduitM a [a] m () 的管道(暂时忽略约束)。因此,当您将 length 映射到它上面时,您会得到 ConduitM a Int m ()a 仍然存在于第一个类型参数中,因此 FromField a 约束可以保留,等待在使用站点实例化。

pipeline2 是一个类型类似于 ConduitM BS.ByteString [a] m () 的管道。现在,如果您将 length 映射到它上面,您将得到 ConduitM BS.ByteString Int m ()。在该类型的任何地方都没有 a,因此无法在使用站点选择 FromField a 实例。相反,必须立即选择它。但是 pipeline2 .| mapC length 中没有任何内容说明 a 应该是什么。这就是为什么它抱怨 a 模棱两可。

据我所知(对管道不是很熟悉),这应该也是您第一个定义的唯一问题。 FromRecord 不保证 Foldable,但它有 Foldable 的实例;您只需要确定正在使用的类型,因为 length 不会这样做。你可以在 pipeline 上使用表达式签名,当你使用它时,TypeApplication 扩展,一个较少多态的定义(不需要像 pipeline2 这样的重新实现;你可以有 pipeline' = pipeline 如果你在 pipeline' 上有正确的签名)。

你遇到的错误...

 • Could not deduce (FromField a0) arising from a use of ‘pipeline2’
  from the context: MonadError CsvParseError m
    bound by the inferred type of
             it :: MonadError CsvParseError m => ConduitM BS.ByteString Int m ()
    at <interactive>:1:1
  The type variable ‘a0’ is ambiguous

... 表示 a0 是不明确的,这使得无法确定应该使用 FromField 的哪个实例。是什么让它模棱两可?错误消息还提到了表达式的推断类型:

it :: MonadError CsvParseError m => ConduitM BS.ByteString Int m ()

该类型没有a0。这会导致歧义,因为没有可以指定 FromField 实例的这种类型的专门化——没有足够的 material 供类型检查器使用。另一方面,在您的第三个示例中...

pipeline3 .| mapC length
  :: (FromField a, MonadError CsvParseError m) => ConduitM a Int m ()

...字段的类型确实显示在整体类型中,因此避免了歧义。

值得强调的是,pipeline2本身并没有什么问题。问题的出现只是因为 length 从整体类型中删除了有用的信息。相比之下,例如,这个工作得很好:

GHCi> :t pipeline2 .| mapC id
pipeline2 .| mapC id
  :: (MonadError CsvParseError m, FromField a) =>
     ConduitM BS.ByteString [a] m ()

为了将pipeline2length一起使用,您需要通过类型注释指定字段的类型:

GHCi> -- Arbitrary example.
GHCi> :t (pipeline2 :: MonadError CsvParseError m => ConduitM BS.ByteString [Int] m ()) .| mapC length
(pipeline2 :: MonadError CsvParseError m => ConduitM BS.ByteString [Int] m ()) .| mapC length
  :: MonadError CsvParseError m => ConduitM BS.ByteString Int m ()

注释的替代方法包括使用 TypeApplications 扩展(感谢 ben 的回答提醒我这一点)...

GHCi> :set -XTypeApplications 
GHCi> :t pipeline2 @_ @Int .| mapC length
pipeline2 @_ @Int .| mapC length
  :: MonadError CsvParseError m => ConduitM BS.ByteString Int m ()

...并通过代理参数指定字段类型。

{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleContexts  #-}

import Data.Proxy
-- etc.

rowLength :: forall m a. (MonadError CsvParseError m, FromField a)
    => Proxy a -> ConduitM BS.ByteString Int m ()
rowLength _ = p2 .| mapC length
    where
    p2 :: (MonadError CsvParseError m, FromField a)
        => ConduitM BS.ByteString [a] m ()
    p2 = pipeline2
GHCi> :t rowLength (Proxy :: Proxy Int)
rowLength (Proxy :: Proxy Int)
  :: MonadError CsvParseError m => ConduitM BS.ByteString Int m ()