【问题标题】:Typeclass constraints, polymorphism and cassava-conduit类型类约束、多态性和木薯管道
【发布时间】:2016-11-24 08:08:21
【问题描述】:

在玩 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 有问题?有没有办法在不进一步指定管道输出类型的情况下定义最通用的“管道”? 我认为FromField 对象的列表就足够了。

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

非常感谢您的回答!

【问题讨论】:

    标签: haskell polymorphism typeclass conduit


    【解决方案1】:

    你得到的错误...

     • 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 实例的这种类型的专门化——没有足够的材料供类型检查器使用。另一方面,在您的第三个示例中...

    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 ()
    

    【讨论】:

      【解决方案2】:

      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')。

      【讨论】:

      • 感谢您提醒我TypeApplications。我举起这个答案。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-06
      • 1970-01-01
      • 1970-01-01
      • 2014-12-18
      • 1970-01-01
      • 2012-09-06
      相关资源
      最近更新 更多