让我们仔细比较hashFile
的类型和categorize
的类型,记住我们想将hashFile
作为参数传递给categorize
hashFile :: FilePath -> IO (Digest MD5)
categorize :: Ord k => ( a -> k ) -> [a] -> M.Map k [a]
categorize hashFile
无法通过类型检查,因为 GHC 会尝试将
k
与
IO (Digest MD5)
匹配,但是
IO
没有
Ord
实例。换句话说,
IO (Digest MD5)
作为
Map
的键是无用的:你需要的是
Digest MD5
,而不是最终执行时才会生成
Digest MD5
的计算。
你真正想做的是运行所有的 IO
计算并将它们的结果(类型为 Digest MD5
)放入 Map
中。得到的函数将返回 IO (Map (Digest MD5) FilePath)
- 一个 IO
计算,当你运行它时,它将返回一个 Map (Digest MD5) FilePath
。
最简单的方法是调整 categorize
来适应我们需要的类型。
categorize :: (Applicative f, Ord k) => (a -> f k) -> [a] -> f (M.Map k a)
categorize f = fmap M.fromList . traverse (\x -> fmap (, x) (f x))
我正在使用TupleSections
。首先让我们来看一下类型。由于IO
是Applicative
的实例,因此在以下约束条件下,(a -> f k)
与FilePath -> IO (Digest MD5)
匹配:
a ~ FilePath
f ~ IO
k ~ Digest MD5
现在让我们来看一下实现。traverse :: Applicative f => (a -> f b) -> [a] -> f [b]
*(原名mapM
)获取一个Applicative
函数,将其映射到列表上,并将结果合并成一个列表。我们使用它把每个项目变成一个(result, item)
元组。这将生成一个f [(k, a)]
值。然后我在结果上应用fmap
M.fromList
来产生f (M.Map k a)
。
*技术上,traverse
有更一般的类型(Applicative f, Traversable t) => (a -> f b) -> t a -> f (t b)
。我通过取 t ~ []
得到了这个版本。
因为这个实现使用了M.fromList
,所以重复的项目会被丢弃。实际上,如果您不希望任何文件具有相同的内容,那么MD5哈希将是不同的,所以这不会成为问题。练习:如果我们想保留重复项,这会如何改变?
FilePath -> IO (Digest MD5)
。获取一个FilePath -> IO (Digest MD5, FilePath)
[IO (Digest MD5, FilePath)]
列表IO [(Digest MD5, FilePath)]
categorize
来获取所需的IO (Map (Digest MD5) [FilePath])