我正在尝试一个Haskell编码挑战,其中给定一个带有前缀的字符串,指示哪些子字符串是分隔标记,需要从输入构建一个列表。
我已经解决了多个单长度分隔符的问题,但我遇到了分隔符可以是任意长度的问题。我使用Data.List.Split中的splitOneOf,但这仅适用于字符(长度为1)分隔符。
例如,给定
输入 ";,\n1;2,3,4;10"
,
分隔符为';'
和','
拆分上述传输中的输入
输出 [1,2,3,4,10]
我面临的问题有两部分:
首先,任何长度的单个分隔符,例如
"****\n1****2****3****4****10"
应该会生成列表[1,2,3,4,10]
。
其次,可以指定多个分隔符,例如
输入 "[***][||]\n1***2||3||4***10"
,
分隔符为"***"
和"||"
拆分上述传输中的输入
输出 [1,2,3,4,10]
我的代码用于在字符分隔符的情况下检索分隔符:
--This gives the delimiters as a list of characters, i.e. a String.
getDelimiter::String->[Char]
getDelimiter text = head . splitOn "\n" $ text
--drop "[delimiters]\n" from the input
body::String->String
body text = drop ((length . getDelimiter $ text)+1)) $ text
--returns tuple with fst being the delimiters, snd the body of the input
doc::String->(String,String)
doc text = (getDelimiter text, body text)
--given the delimiters and the body of the input, return a list of strings
numbers::(String,String)->[String]
numbers (delim, rest) = splitOneOf delim rest
--input ",@#\n1,2@3#4" gives output ["1","2","3","4"]
getList::String->[String]
getList text = numbers . doc $ text
所以我的问题是,如何对分隔符进行处理,例如"***"
和"||"
?
欢迎任何提示,特别是在函数式编程环境中。
答案 0 :(得分:2)
这个问题有两种解决方案:简单,高效。我不会介绍效率(因为它并不简单),虽然我会暗示它。
但首先,您可以使用delimiter
简化提取输入的body
和Data.List.break
部分的部分:
delims = splitOn "/" . fst . break (== '\n') -- Presuming the delimiters are delimited with
-- a slash.
body = snd . break (== '\n')
无论如何,我们可以将此问题减少为 查找 所有给定 模式 的位置给定的 字符串 。 (通过说“string”,我不是指haskell String
。而是指任何 符号的任意长序列(甚至是无限 stream ) ,其中定义了Eq
uality关系,在Haskell中输入为Eq a => [a]
。我希望这不会太混乱。)一旦我们有了职位,我们可以将字符串切成我们心中的内容。如果我们想要处理无限流,我们必须逐步获得位置,并在我们前进时产生结果,这是必须牢记的限制。 Haskell配备足以处理流案例和有限字符串。
简单方法是对每个模式在字符串上转换isPrefixOf
。
Nothing
。Just
并移至下一个位置。因此,我们将用一个替换所有不同的分隔符:Nothing
。然后我们可以通过它轻松切割字符串。
这是相当惯用的,我会尽快将代码提交给你。这种方法的问题在于效率低下:实际上,如果模式不匹配,我们宁愿前进多个符号。
更高效将我们的研究工作建立在用字符串查找模式的研究基础之上;这个问题是众所周知的,有很多复杂的算法可以将它解决得快一个数量级。这些算法旨在使用单一模式,因此必须进行一些工作以使它们适应我们的情况;但是,我相信它们具有适应性。最简单和最老的这类算法是KMP,它在Haskell中已经是encoded。你可能希望拿起武器并概括它 - 快速通往一定数量的名望。
以下是代码:
module SplitSubstr where
-- stackoverflow.com/questions/49228467
import Data.List (unfoldr, isPrefixOf, elemIndex)
import Data.List.Split (splitWhen) -- Package `split`.
import Data.Maybe (catMaybes, isNothing)
-- | Split a (possibly infinite) string at the occurrences of any of the given delimiters.
--
-- λ take 10 $ splitOnSubstrs ["||", "***"] "la||la***fa"
-- ["la","la","fa"]
--
-- λ take 10 $ splitOnSubstrs ["||", "***"] (cycle "la||la***fa||")
-- ["la","la","fa","la","la","fa","la","la","fa","la"]
--
splitOnSubstrs :: [String] -> String -> [String]
splitOnSubstrs delims
= fmap catMaybes -- At this point, there will be only `Just` elements left.
. splitWhen isNothing -- Now we may split at nothings.
. unfoldr f -- Replace the occurences of delimiters with a `Nothing`.
where
-- | This is the base case. It will terminate the `unfoldr` process.
f [ ] = Nothing
-- | This is the recursive case. It is divided into 2 cases:
-- * One of the delimiters may match. We will then replace it with a Nothing.
-- * Otherwise, we will `Just` return the current element.
--
-- Notice that, if there are several patterns that match at this point, we will use the first one.
-- You may sort the patterns by length to always match the longest or the shortest. If you desire
-- more complicated behaviour, you must plug a more involved logic here. In any way, the index
-- should point to one of the patterns that matched.
--
-- vvvvvvvvvvvvvv
f body@(x:xs) = case elemIndex True $ (`isPrefixOf` body) <$> delims of
Just index -> return (Nothing, drop (length $ delims !! index) body)
Nothing -> return (Just x, xs)
可能会发现您不会直截了当地找到此代码。具体来说,unfoldr
部分有点密集,所以我将添加一些关于它的文字。
unfoldr f
是递归方案的实施例。 f
是一个可以填充body
:f :: (body -> Maybe (chip, body))
的一部分的函数。
unfoldr
会继续将其应用于body
。这称为递归案例。Nothing
),unfoldr
就会停下并递交你收集的所有筹码。这称为基本案例。在我们的例子中,f
从字符串中获取符号,并在字符串为空时失败。
就是这样。我希望你在快速分割算法获得图灵奖时给我寄明信片。
答案 1 :(得分:2)
如果您不介意对输入字符串进行多次传递,可以使用splitOn
中的Data.List.Split
,并逐步使用一个分隔符逐步分割输入字符串。
您可以使用foldl'
:
import Data.List
import Data.List.Split
splitOnAnyOf :: Eq a => [[a]] -> [a] -> [[a]]
splitOnAnyOf ds xs = foldl' (\ys d -> ys >>= splitOn d) [xs] ds
此处,折叠操作的累加器是一个字符串列表,或更一般地[[a]]
,因此您必须使用xs
将[xs]
“提升”到列表中。 / p>
然后折叠分隔符ds
- 而不是要解析的输入字符串。对于每个分隔符d
,您使用splitOn
分割累积的字符串列表,并将它们连接起来。您也可以使用concatMap
,但在这里我随意选择使用更通用的>>=
( bind )运算符。
这似乎符合OP的要求:
*Q49228467> splitOnAnyOf [";", ","] "1;2,3,4;10"
["1","2","3","4","10"]
*Q49228467> splitOnAnyOf ["***", "||"] "1***2||3||4***10"
["1","2","3","4","10"]
由于这会对临时列表进行多次传递,因此很可能不是您可以进行的最快的实现,但如果您没有太多的分隔符或非常长的列表,这可能就足够了。