【问题标题】:Haskell: Scan Through a List and Apply A Different Function for Each ElementHaskell:扫描列表并为每个元素应用不同的函数
【发布时间】:2012-03-13 15:03:32
【问题描述】:

我需要扫描一个文档并为文件中的每个字符串累积不同函数的输出。在文件的任何给定行上运行的函数取决于该行中的内容。

通过对我想要收集的每个列表进行完整的文件传递,我可以非常低效地做到这一点。示例伪代码:

at :: B.ByteString -> Maybe Atom
at line
    | line == ATOM record = do stuff to return Just Atom
    | otherwise = Nothing

ot :: B.ByteString -> Maybe Sheet
ot line
    | line == SHEET record = do other stuff to return Just Sheet
    | otherwise = Nothing

然后,我会将这些函数中的每一个映射到文件中的整个行列表,以获得原子和表格的完整列表:

mapper :: [B.ByteString] -> IO ()
mapper lines = do
    let atoms = mapMaybe at lines
    let sheets = mapMaybe to lines
    -- Do stuff with my atoms and sheets

但是,这是低效的,因为我正在为我尝试创建的每个列表映射整个字符串列表。相反,我只想在行字符串列表中映射一次,在我移动时识别每一行,然后应用适当的函数并将这些值存储在不同的列表中。

我的C心态想做这个(伪代码):

mapper' :: [B.ByteString] -> IO ()
mapper' lines = do
    let atoms = []
    let sheets = []
    for line in lines:
        | line == ATOM record = (atoms = atoms ++ at line)
        | line == SHEET record = (sheets = sheets ++ ot line)
    -- Now 'atoms' is a complete list of all the ATOM records
    --  and 'sheets' is a complete list of all the SHEET records

Haskell 的方法是什么?我根本无法让我的函数式编程思维提出解决方案。

【问题讨论】:

  • 这几乎是正确的,只需将atomssheets 作为累加器变量传递,最后作为元组返回。
  • 我不确定我是否遵循。我上面的伪代码在 Haskell 中是无稽之谈。 Haskell 不仅没有 for 循环,而且在 do 结构中也没有保护。此外,我对返回元组并不感兴趣。上面显示的是一个例子。如果我想返回 100 种不同类型的列表怎么办?
  • 这就是我说almost的原因。如果您不知道需要生成的列表的数量和种类,则无法在一个函数中完成。就是这么简单。请记住,您的函数需要有一个类型。如果,OTOH,您知道它始终是一个 [a] 列表,并且所有提取函数都是相同的 a,您可以返回列表列表或列表映射。
  • 你不能有data ParseResult = PAtom Atom | PSheet Sheet,映射一个B.ByteString -> ParseResult,然后定义(patoms, rest) = partition isAtom parseResatoms = fromPAtom patomssheets = fromPSheet rest吗?
  • @Ptival:你的回答真的很有趣。你能把它充实起来吗?我真的很喜欢使用条件数据类型的想法,但我不确定您是如何使用它们的。

标签: list haskell functional-programming


【解决方案1】:

首先,我认为其他人提供的答案至少在 95% 的情况下都有效。使用适当的数据类型(或在某些情况下为元组)为手头的问题编写代码始终是一种好习惯。但是,有时您确实事先并不知道要在列表中查找什么,在这些情况下,尝试列举所有可能性是困难的/耗时的/容易出错的。或者,您正在编写同一类事物的多个变体(手动将多个折叠内联到一个中)并且您想捕获抽象。

幸运的是,有一些技巧可以提供帮助。

框架解决方案

(有点自我宣传)

首先,各种“iteratee/enumerator”包通常提供处理此类问题的函数。我最熟悉iteratee,它可以让您执行以下操作:

import Data.Iteratee as I
import Data.Iteratee.Char
import Data.Maybe

-- first, you'll need some way to process the Atoms/Sheets/etc. you're getting
-- if you want to just return them as a list, you can use the built-in
-- stream2list function

-- next, create stream transformers
-- given at :: B.ByteString -> Maybe Atom
-- create a stream transformer from ByteString lines to Atoms
atIter :: Enumeratee [B.ByteString] [Atom] m a
atIter = I.mapChunks (catMaybes . map at)

otIter :: Enumeratee [B.ByteString] [Sheet] m a
otIter = I.mapChunks (catMaybes . map ot)

-- finally, combine multiple processors into one
-- if you have more than one processor, you can use zip3, zip4, etc.
procFile :: Iteratee [B.ByteString] m ([Atom],[Sheet])
procFile = I.zip (atIter =$ stream2list) (otIter =$ stream2list)

-- and run it on some data
runner :: FilePath -> IO ([Atom],[Sheet])
runner filename = do
  resultIter <- enumFile defaultBufSize filename $= enumLinesBS $ procFile
  run resultIter

这给您带来的一个好处是额外的可组合性。您可以根据需要创建转换器,并将它们与 zip 结合起来。如果你愿意,你甚至可以并行运行消费者(尽管只有当你在 IO monad 中工作,除非消费者做很多工作,否则可能不值得)通过更改为:

import Data.Iteratee.Parallel

parProcFile = I.zip (parI $ atIter =$ stream2list) (parI $ otIter =$ stream2list)

这样做的结果与单个 for 循环不同 - 这仍然会执行数据的多次遍历。但是,遍历模式已经改变。这将一次加载一定数量的数据(defaultBufSize 字节)并多次遍历该块,并根据需要存储部分结果。在一个块被完全消耗后,下一个块被加载,旧的块可以被垃圾回收。

希望这将证明差异:

Data.List.zip:
  x1 x2 x3 .. x_n
                   x1 x2 x3 .. x_n

Data.Iteratee.zip:
  x1 x2      x3 x4      x_n-1 x_n
       x1 x2      x3 x4           x_n-1 x_n

如果你做了足够多的工作,并行性是有意义的,这根本不是问题。由于内存局部性,性能比Data.List.zip 对整个输入进行多次遍历要好得多。

美丽的解决方案

如果单遍历解决方案确实最有意义,您可能会对 Max Rabkin 的 Beautiful Folding 帖子和 Conal Elliott 的 followup work (this too) 感兴趣。基本思想是您可以创建数据结构来表示折叠和拉链,并且将它们组合起来可以创建一个新的组合折叠/拉链功能,只需要一次遍历。对于 Haskell 初学者来说,这可能有点高级,但是由于您正在考虑这个问题,您可能会发现它很有趣或有用。 Max 的帖子可能是最好的起点。

【讨论】:

  • 这是一个很棒的回应。谢谢你。我会研究这些选项。
  • Max 和 Conal 的帖子很有趣,感谢您的回答!
【解决方案2】:

引入新的 ADT 是个好主意,例如“摘要”而不是元组。 然后,由于您想累积 Summary 的值,因此将其设为 Data.Monoid 的一个实例。然后,借助分类器函数(例如 isAtom、isSheet 等)对每一行进行分类,并使用 Monoid 的 mconcat 函数将它们连接在一起(如 @dave4420 所建议的那样)。

这是代码(它使用String而不是ByteString,但很容易更改):

module Classifier where

import Data.List
import Data.Monoid

data Summary = Summary
  { atoms :: [String]
  , sheets :: [String]
  , digits :: [String]
  } deriving (Show)

instance Monoid Summary where
  mempty = Summary [] [] []
  Summary as1 ss1 ds1 `mappend` Summary as2 ss2 ds2 =
    Summary (as1 `mappend` as2)
            (ss1 `mappend` ss2)
            (ds1 `mappend` ds2)

classify :: [String] -> Summary
classify = mconcat  . map classifyLine

classifyLine :: String -> Summary
classifyLine line
  | isAtom line  = Summary [line] [] [] -- or "mempty { atoms = [line] }"
  | isSheet line = Summary [] [line] []
  | isDigit line = Summary [] [] [line]
  | otherwise    = mempty -- or "error" if you need this  

isAtom, isSheet, isDigit :: String -> Bool
isAtom = isPrefixOf "atom"
isSheet = isPrefixOf "sheet"
isDigit = isPrefixOf "digits"

input :: [String]
input = ["atom1", "sheet1", "sheet2", "digits1"]

test :: Summary
test = classify input

【讨论】:

  • 是的,我喜欢新数据结构的想法,而不是 n 元组。
【解决方案3】:

我展示了两种线的解决方案,但是通过使用五元组而不是二元组,它很容易扩展到五种线。

import Data.Monoid

eachLine :: B.ByteString -> ([Atom], [Sheet])
eachLine bs | isAnAtom bs = ([ {- calculate an Atom -} ], [])
            | isASheet bs = ([], [ {- calculate a Sheet -} ])
            | otherwise = error "eachLine"

allLines :: [B.ByteString] -> ([Atom], [Sheet])
allLines bss = mconcat (map eachLine bss)

魔术是由来自Data.Monoidmconcat 完成的(包含在GHC 中)。

(就风格而言:我个人会定义一个Line 类型、一个parseLine :: B.ByteString -&gt; Line 函数并编写eachLine bs = case parseLine bs of ...。但这是您的问题的外围。)

【讨论】:

  • 一个 n 元组非常可疑。我非常希望避免这样的事情,但我现在没有其他选择。
  • 对于一小段本地代码,一个很好的 n 元组 (imo)。如果要在程序中更广泛地传递它,我会定义一个新类型,正如@dying_sphynx 建议的那样,但如果这就是它的全部用途,那么开销太大了。 (在任何情况下,新类型都与元组同构。)
【解决方案4】:

如果您只有 2 个选择,使用 Either 可能是个好主意。在这种情况下,组合你的函数,映射列表,并使用左右来获得结果:

import Data.Either

-- first sample function, returning String
f1 x = show $ x `div` 2

-- second sample function, returning Int
f2 x = 3*x+1

-- combined function returning Either String Int
hotpo x = if even x then Left (f1 x) else Right (f2 x)

xs = map hotpo [1..10] 
-- [Right 4,Left "1",Right 10,Left "2",Right 16,Left "3",Right 22,Left "4",Right 28,Left "5"]

lefts xs 
-- ["1","2","3","4","5"]

rights xs
-- [4,10,16,22,28]

【讨论】:

  • 没有。我需要返回的列表类型的数量将不限于 2。这只是一个示例。目前,我实际上想要收集 5 种列表类型。
猜你喜欢
  • 2019-12-25
  • 2015-02-22
  • 1970-01-01
  • 2013-03-21
  • 1970-01-01
  • 1970-01-01
  • 2014-09-24
  • 1970-01-01
相关资源
最近更新 更多