Haskell 类型类约束、多态性和木薯导管

Haskell 类型类约束、多态性和木薯导管,haskell,polymorphism,typeclass,conduit,Haskell,Polymorphism,Typeclass,Conduit,在玩Haskell和导管的时候,我遇到了一个我很难解释的行为。首先,让我列出重现问题所需加载的所有模块和语言扩展: {-# LANGUAGE FlexibleContexts #-} import Conduit -- conduit-combinators import Data.Csv -- cassava import Data.Csv.Conduit -- ca

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

{-# 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
pipeline2 :: (MonadError CsvParseError m, FromField a)
          => ConduitM BS.ByteString [a] m ()
pipeline2 = fromCsv defaultDecodeOptions NoHeader
然后,我想输出我的csv文件中每一行的元素数量——我知道这有点愚蠢和无用,还有十亿种其他的方法来做这类事情,但那只是一个玩具测试

所以我打开了GHCi并尝试了以下方法:

ghci> :t pipeline .| mapC length
正如预期的那样,这不起作用,因为Record a的约束
不能保证
a
是可折叠的。因此,我定义了以下管道:

pipeline :: (MonadError CsvParseError m, FromRecord a)
         => ConduitM BS.ByteString a m ()
pipeline = fromCsv defaultDecodeOptions NoHeader
pipeline2 :: (MonadError CsvParseError m, FromField a)
          => ConduitM BS.ByteString [a] m ()
pipeline2 = fromCsv defaultDecodeOptions NoHeader
这是一个合法的定义,因为根据木薯文档,
FromField a=>FromField[a]
FromRecord
的一个实例

在这一点上,我感到高兴和充满希望,因为
[]
可折叠的
的一个例子。因此,我再次打开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
我再次打开GHCi并尝试:

ghci> :t pipeline3 .| mapC length
这次我得到:

pipeline3 .| mapC length
  :: (FromField a, MonadError CsvParseError m) => ConduitM a Int m ()
因此,这一次,GHCi明白我不必进一步指定
pipeline3
的定义

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

我觉得我忽略了关于类型类以及如何以多态方式组合函数或导管对象的一个要点


非常感谢您的回答

pipeline3
是一种类型类似于
ConduitM a[a]m()
的导管(暂时忽略约束)。因此,当你把
长度映射到它上面时,你会得到
一个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
不保证可折叠,但它有可折叠的实例;您只需要确定正在使用的类型,因为
length
不起作用。使用时,可以在
管道上使用表达式签名,即
类型应用程序
扩展,这是一个多态性较小的定义(不需要像
管道2
那样重新实现;如果在
管道上有正确的签名
,则可以使用
管道
).

您得到的错误

 • 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
实例——没有足够的材料供类型检查器使用。另一方面,在你的第三个例子中

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 ()
要将
pipeline2
length
一起使用,需要通过类型注释指定字段的类型:

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的回答提醒了我这一点)

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

{-# 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

感谢您提醒我有关
类型应用程序的信息。我把它举起来作为我的答案。
{-# 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 ()