Haskell:使用MapReduce搜索子字符串?

时间:2013-03-03 19:37:33

标签: haskell

我正在尝试使用现有的MapReduce实现编写一个简单的程序( Real World Haskell 中的实现)。

作为使用框架的一个例子,这里有一些代码来计算文件中的单词数:

module Main where

import Control.Monad (forM_)
import Data.Int (Int64)
import qualified Data.ByteString.Lazy.Char8 as LB
import System.Environment (getArgs)

import LineChunks (chunkedReadWith)
import MapReduce (mapReduce, rdeepseq)

wordCount :: [LB.ByteString] -> Int
wordCount = mapReduce rdeepseq (length . LB.words)
                      rdeepseq sum

main :: IO ()
main = do
  args <- getArgs
  forM_ args $ \path -> do
    numWords <- chunkedReadWith wordCount path
    putStrLn $ "Words: " ++ show numWords

我需要使用相同的MapReduce框架来编写一个搜索某些字符串的程序(比如“il”),然后返回找到它们的行号。例如,输出可能是:

verILy: found on lines 34, 67, 23
ILlinois: found on lines 1, 2, 56
vILla: found on lines 4, 5, 6

(“il”的大写不是必需的。)

我是Haskell的初学者,还没有任何具体的想法。我注意到Data.ByteString.Lazy.Char8类有一个成员函数findIndices。是否可以使用它?

非常感谢正确方向的任何代码或提示。

1 个答案:

答案 0 :(得分:3)

好的,让我们点击这个。


首先,我们需要一种方法来查找字符串中的单词。这可以使用Regex来完成,比方说regex-tdfa包。 Haskell正则表达式包很不错,但是非常通用,一开始有点难以阅读。我们将创建一个函数,它只是匹配运算符(=~)的一个包装器,主要是为了使类型具体化。

wordHere :: LB.ByteString -> LB.ByteString -> Bool
wordHere word string = string =~ word
-- a.k.a.            = flip (=~)

现在,mapReduce通过向不同的火花提供大量([a] -> c)类型的并行本地“映射”作业来分解像(a -> b)这样的列表,然后将reduce作业中的所有结果折叠为([b] -> c)。一般来说,不能保证减少步骤的 order ,但RWH的mapReduce实际上确实给了我们保证。

RWH的lineChunks函数实际上将文档(LB.ByteString - &gt; [LB.ByteString])拆分为少量行的块。我们的映射作业各自获得其中一个块,并且需要在本地提供有关行匹配的信息。我们可以通过将块分成它们的组成行来进行,本地对行进行编号,将wordHere映射到它们上,并收集wordHere返回true的本地行号。我们通常会首先执行此操作,将wordHere替换为任何谓词p :: (LB.ByteString -> Bool)

import Control.Arrow

localLinesTrue :: (LB.ByteString -> Bool) -> [LB.ByteString] -> [Int]
localLinesTrue p ls = map fst . filter snd . map (second p) . zip [1..]

现在我们可以创建像localLinesTrue (wordHere $ LB.pack "foobar") . LB.lines :: LB.ByteString -> [Int]这样的本地地图制作工具。

给定该类型的映射器也会稍微阐明函数其余部分的类型。我们现在有

>>> :t mapReduce rdeepseq (localLinesTrue (wordHere "foo")) rdeepseq
...    :: ([[Int]] -> c) -> [LB.ByteString] -> c

因此我们的reducer必须是([[Int]] -> c)类型。很酷,让我们尝试做到这一点。如果我们有一个Int列表,我们可以重建实际的行号......

[[], [], [], [5], [3], [], []]
等等,实际上,我们做不到。我们需要在mapper中添加更多信息 - 每个块中出现的行数。幸运的是,由于我们小心翼翼地保持我们的东西解开,这很容易添加。我们需要一个更像([Int], Int)的返回类型,其中第二个参数是该块的行数。我们可以通过“扇出”(&&&)来做到这一点。

mapper regex = (localLinesTrue (wordHere regex) &&& length) . LB.lines

现在我们的输出看起来像

[ ([], 3),  ([], 5),  ([3, 5], 10), ... ]

我们可以实现一个使用State monad

计算的reducer
reducerStep :: ([Int], Int) -> State Int [Int]
reducerStep (hits, count) = do pos <- get
                               modify (+count)
                               return (map (+pos) hits)

reducer :: [([Int], Int)] -> [Int]
reducer = concat . evalState 0 . mapM reducerStep

我们已经

mapReduce rdeepseq (mapper regex)
          rdeepseq reducer
  :: [LB.ByteString] -> [Int]

应该让你在95%的路上走到最后。