parágrafos funcionais
-
06-09-2019 - |
Pergunta
Desculpe eu não começ completamente FP ainda, eu quero dividir uma seqüência de linhas em uma seqüência de seqüências de linhas, assumindo uma linha em branco como a divisão parágrafo, eu poderia fazê-lo em python como este:
def get_paraghraps(lines):
paragraphs = []
paragraph = []
for line in lines:
if line == "": # I know it could also be "if line:"
paragraphs.append(paragraph)
paragraph = []
else:
paragraph.append(line)
return paragraphs
Como você vai fazer sobre isso em Erlang ou Haskell?
Solução
Sou apenas um programador Haskell começando (e o pequeno Haskell que eu aprendi foi 5 anos atrás), mas para um começo, eu ia escrever a tradução natural de sua função, com o acumulador ( "o parágrafo atual") sendo repassados ??(Eu adicionei os tipos, apenas para maior clareza):
type Line = String
type Para = [Line]
-- Takes a list of lines, and returns a list of paragraphs
paragraphs :: [Line] -> [Para]
paragraphs ls = paragraphs2 ls []
-- Helper function: takes a list of lines, and the "current paragraph"
paragraphs2 :: [Line] -> Para -> [Para]
paragraphs2 [] para = [para]
paragraphs2 ("":ls) para = para : (paragraphs2 ls [])
paragraphs2 (l:ls) para = paragraphs2 ls (para++[l])
Isso funciona:
*Main> paragraphs ["Line 1", "Line 2", "", "Line 3", "Line 4"]
[["Line 1","Line 2"],["Line 3","Line 4"]]
Então, isso é uma solução. Mas, em seguida, a experiência Haskell sugere que há quase sempre funções de biblioteca para fazer coisas como esta :) Uma função relacionada é chamada groupBy , e quase funciona:
paragraphs3 :: [Line] -> [Para]
paragraphs3 ls = groupBy (\x y -> y /= "") ls
*Main> paragraphs3 ["Line 1", "Line 2", "", "Line 3", "Line 4"]
[["Line 1","Line 2"],["","Line 3","Line 4"]]
Opa. O que realmente precisamos é um "splitBy" e não é nas bibliotecas , mas podemos filtrar os maus-nos:
paragraphs4 :: [Line] -> [Para]
paragraphs4 ls = map (filter (/= "")) (groupBy (\x y -> y /= "") ls)
ou, se você quiser ser legal, você pode se livrar do argumento e fazê-lo da maneira inútil:
paragraphs5 = map (filter (/= "")) . groupBy (\x y -> y /= "")
Eu tenho certeza que há uma maneira ainda mais curto. :-)
Editar : ephemient pontos que (not . null)
é mais limpo do que (/= "")
. Assim, podemos escrever
paragraphs = map (filter $ not . null) . groupBy (const $ not . null)
O (not . null)
repetida é uma forte indicação de que nós realmente deveria abstrato isso em uma função, e é isso que o Data.List.Split módulo faz, como apontado na resposta abaixo.
Outras dicas
Eu também estou tentando aprender Haskell. Uma solução para esta questão poderia ser:
paragraphs :: [String] -> [[String]]
paragraphs [] = []
paragraphs lines = p : (paragraphs rest)
where (p, rest) = span (/= "") (dropWhile (== "") lines)
onde eu estou usando as funções de Dados .list. O que eu estou usando já estão disponíveis a partir do Prelude, mas você pode encontrar a sua documentação no link.
A idéia é encontrar o primeiro parágrafo usando span (/= "")
. Isso irá retornar o parágrafo, e as linhas seguintes. Em seguida, recurse na lista menor de linhas que eu chamo rest
.
Antes de divisão para fora no primeiro parágrafo, que descartar quaisquer linhas vazias usando dropWhile (== "")
. Isto é importante para comer a linha de vazio (s) separando os parágrafos. Minha primeira tentativa foi o seguinte:
paragraphs :: [String] -> [[String]]
paragraphs [] = []
paragraphs lines = p : (paragraphs $ tail rest)
where (p, rest) = span (/= "") lines
mas isso falhar quando chegarmos ao parágrafo final desde rest
é então a string vazia:
*Main> paragraphs ["foo", "bar", "", "hehe", "", "bla", "bla"] [["foo","bar"],["hehe"],["bla","bla"]*** Exception: Prelude.tail: empty list
Dropping linhas vazias resolve isso, e ele também faz o deleite código de qualquer número de linhas em branco como um separador de parágrafo, que é o que eu esperaria de um usuário.
Pense de forma recursiva.
get_paragraphs [] paras para = paras ++ [para]
get_paragraphs ("":ls) paras para = get_paragraphs ls (paras ++ [para]) []
get_paragraphs (l:ls) paras para = get_paragraphs ls paras (para ++ [l])
Você deseja agrupar as linhas, então groupBy
de Data.List
parece ser um bom candidato. Ele usa uma função personalizada para determinar quais linhas são "iguais" para que se possa fornecer algo que faz linhas do mesmo parágrafo "igual". Por exemplo:
import Data.List( groupBy )
inpara :: String -> String -> Bool
inpara _ "" = False
inpara _ _ = True
paragraphs :: [String] -> [[String]]
paragraphs = groupBy inpara
Isto tem algumas limitações, uma vez inpara
só pode comparar duas linhas adjacentes e lógica mais complexa não se encaixa no quadro dado por groupBy
. Uma solução mais elementar se é mais flexível. Usando um básico recursão pode escrever:
paragraphs [] = []
paragraphs as = para : paragraphs (dropWhile null reminder)
where (para, reminder) = span (not . null) as
-- splits list at the first empty line
span
divide uma lista no ponto a função fornecida se torna falso (a primeira linha em branco), remove dropWhile
principais elementos para os quais a função fornecida é verdadeira (vazias quaisquer linhas principais).
Melhor tarde do que nunca.
import Data.List.Split (splitOn)
paragraphs :: String -> [[String]]
paragraphs s = filter (not . null) $ map words $ splitOn "\n\n" s
paragraphs "a\nb\n\nc\nd" == [["a", "b"], ["c", "d"]]
paragraphs "\n\na\nb\n\n\nc\nd\n\n\n" == [["a", "b"], ["c", "d"]]
paragraphs "\n\na\nb\n\n \n c\nd\n\n\n" == [["a", "b"], ["c", "d"]]