Haskell 类型类约束、多态性和木薯导管
在玩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
{-# 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 ()