类型类约束、多态性和 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
不能保证 a
是 Foldable
。所以我定义了以下管道:
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 ()
为了将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 的回答提醒我这一点)...
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 ()
在玩 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
不能保证 a
是 Foldable
。所以我定义了以下管道:
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 ()
为了将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 的回答提醒我这一点)...
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 ()