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]