如何在Haskell中组合函数和单子操作

4

我刚开始学习Haskell。

我正在创建一个查找重复文件的程序。我已经创建了以下函数:

hashFile :: (MonadIO m) => FilePath -> m (Digest MD5)
categorize :: Ord k => (a -> k) -> [a] -> Map.Map k [a]

我想把它们组合在一个函数中,该函数返回结果。
Map.Map (Digest MD5) [FilePath]

我的问题是:

我找不到处理IO单子以获取所需内容的方法。因此,我的问题如下:

  1. 我尝试做的是否正确,或者返回类型真的应该是Map.Map (IO (Digest MD5)) [FilePath]

  2. 如何将这些函数组合在一起,以获得按哈希值分组的文件列表?


2
  1. 你有一个 FilePath -> IO (Digest MD5)。获取一个 FilePath -> IO (Digest MD5, FilePath)
  2. 接下来,建立一个 [IO (Digest MD5, FilePath)] 列表
  3. 使用 Traversable 类获得一个 IO [(Digest MD5, FilePath)]
  4. 使用 Functor 和 categorize 来获取所需的 IO (Map (Digest MD5) [FilePath])
- hao
1个回答

6

让我们仔细比较hashFile的类型和categorize的类型,记住我们想将hashFile作为参数传递给categorize

hashFile ::             FilePath -> IO (Digest MD5)  -- I simplified the MonadIO constraint
categorize :: Ord k => (    a    ->        k       ) -> [a] -> M.Map k [a]
categorize hashFile 无法通过类型检查,因为 GHC 会尝试将 kIO (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。首先让我们来看一下类型。由于IOApplicative的实例,因此在以下约束条件下,(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)]值。然后我在结果上应用fmapM.fromList来产生f (M.Map k a)

*技术上,traverse有更一般的类型(Applicative f, Traversable t) => (a -> f b) -> t a -> f (t b)。我通过取 t ~ [] 得到了这个版本。

因为这个实现使用了M.fromList,所以重复的项目会被丢弃。实际上,如果您不希望任何文件具有相同的内容,那么MD5哈希将是不同的,所以这不会成为问题。练习:如果我们想保留重复项,这会如何改变?


@AndPos:“它帮助我理解如何使用单子”,但这里的一个重要点应该是,您不需要单子,应用函子就足够了。 - Cactus
@Cactus 我不同意这是一个重要的观点。这是一个观点,但它是微妙的,对于99%的应用程序来说可能并不重要,相比于理解如何使用与Monad相关的接口来使用IO,这在100%有用的应用程序中需要完成。 - Daniel Wagner
2
@DanielWagner:我认为单子接口在初学者材料中被过度代表,所以我从不放弃倡导应用接口的机会。 - Cactus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接