Haskell: сканирование списка и применение различных функций для каждого элемента

мне нужно сканировать документ и накапливать вывода различных функций для каждой строки в файле. Функция запуска в любой строке файла зависит от того, что находится в этой строке.

я мог бы сделать это очень неэффективно, сделав полный проход через файл для каждого списка, который я хотел собрать. Пример псевдо-кода:

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

каков способ Хаскелла сделать это? Я просто не могу заставить мое мышление функционального программирования придумать решение.

4 ответов


прежде всего, я думаю, что ответы, которые другие предоставили, будут работать по крайней мере 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

один преимущество это дает вам дополнительную composability. Вы можете создавать трансформеры, как вам нравится, и просто комбинировать их с zip. Вы даже можете запускать потребителей параллельно, если хотите (хотя только если вы работаете в IO монада, и, вероятно, не стоит, если потребители не делают много работы), перейдя к этому:

import Data.Iteratee.Parallel

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

результат этого не совпадает с одним for-loop-это все равно будет выполнять несколько обходов данных. Однако, шаблон обхода изменился. Это загрузит определенный объем данных сразу (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 сделает.

прекрасным решением

если однопроходное решение действительно имеет смысл, вас может заинтересовать Макс Рабкин Красивый Складной сообщение, и Конэл Эллиот followup работа (и это тоже). Основная идея заключается в том, что вы можете создавать структуры данных для представления складок и молний и комбинировать их вы создаете новую комбинированную функцию fold / zip, для которой требуется только один обход. Возможно, это немного продвинуто для новичка Haskell, но поскольку вы думаете о проблеме, вы можете найти ее интересной или полезной. Должность Макса, вероятно, лучшая отправная точка.


я показываю решение для двух типов линий, но оно легко расширяется до пяти типов линий с помощью пяти кортежей вместо двух кортежей.

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)

магия делается mconcat С данные.Моноид (в комплекте с GHC).

(по стилю: лично я бы определил Line тип, a и писать eachLine bs = case parseLine bs of .... Но это второстепенный вопрос.)


рекомендуется ввести новый ADT, например "Summary" вместо кортежей. Тогда, поскольку вы хотите накапливать ценности резюме, вы пришли сделать его istance в данных.Моноид. Затем вы классифицируете каждую из своих строк с помощью функций классификатора (например, isAtom, isSheet и т. д.) и объединить их вместе, используя функцию Mconcat моноида (как предложено @dave4420).

вот код (он использует строку вместо 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

Если у вас есть только 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]