在Haskell

时间:2018-03-12 05:11:20

标签: string haskell split functional-programming

我正在尝试一个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

所以我的问题是,如何对分隔符进行处理,例如"***""||"

欢迎任何提示,特别是在函数式编程环境中。

2 个答案:

答案 0 :(得分:2)

这个问题有两种解决方案:简单,高效。我不会介绍效率(因为它并不简单),虽然我会暗示它。

但首先,您可以使用delimiter简化提取输入的bodyData.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是一个可以填充bodyf :: (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"]

由于这会对临时列表进行多次传递,因此很可能不是您可以进行的最快的实现,但如果您没有太多的分隔符或非常长的列表,这可能就足够了。