Haskell - 按分隔符拆分字符串

5

我正在尝试在Haskell中编写一个通过分隔符拆分字符串的程序。

我已经研究了其他用户提供的不同示例。下面是一段示例代码。

split :: String -> [String]
split [] = [""]
split (c:cs)
   | c == ','  = "" : rest
   | otherwise = (c : head rest) : tail rest
 where
   rest = split cs

样例输入:"1,2,3"。 样例输出:["1","2","3"]

我一直在尝试修改代码,以便输出的结果类似于["1", "," , "2", "," , "3"],其中包括分隔符在内,但是我一直无法成功。

例如,我更改了以下行:

   | c == ','  = "" : rest

into:

   | c == ','  = "," : rest

但结果变成了["1,","2,","3"]

问题出在哪里,我哪里理解有误了吗?


不要仅仅修改代码以使其符合您的规格,最好是理解正在发生的事情。你能用自己的话描述一下 split 如何工作吗? - Willem Van Onsem
1
根据我的理解,示例代码分为两个部分。第一部分split [] = [""]是基础情况,当我们尝试拆分空字符串时返回一个包含一个空字符串的列表。第二部分类似于递归,它遍历输入中的每个字符。如果字符是分隔符,则将空字符串""与剩余字符串的拆分结果组合在一起。但我不确定 "| otherwise = (c : head rest) : tail rest" 函数的作用,因为 c 是字符,而 "head rest" 应该返回一个字符串。 - Peter.PP
你可以使用 split = map pure 来实现这个功能。 - Redu
3个回答

8

如果你想真正地编写这个函数而不是为了练习而逐个字符进行递归,我认为更清晰的方法是使用来自 Data.Listbreak 函数。下面是表达式:

break (==',') str

将字符串分解为元组(a,b),其中第一部分包含初始的“不带逗号”的部分,第二部分是以逗号开头的更多字符串或者为空(如果没有更多字符串)。

这使得split的定义变得清晰和简单:

split str = case break (==',') str of
                (a, ',':b) -> a : split b
                (a, "")    -> [a]

你可以验证这样处理 split ""(返回 [""])是可行的,所以没有必要把它视为特殊情况。
这个版本的好处在于添加分隔符的修改也很容易理解:
split2 str = case break (==',') str of
                (a, ',':b) -> a : "," : split2 b
                (a, "")    -> [a]

请注意,我在这些函数中编写模式的详细程度比必要的要高,以确保清楚地传达其含义,这也意味着Haskell会对每个逗号进行重复检查。因此,一些人可能更喜欢使用以下方式:
split str = case break (==',') str of
                (a, _:b) -> a : split b
                (a, _)   -> [a]

或者,如果他们仍然想要记录每个情况分支中他们所期望的内容:
split str = case break (==',') str of
                (a, _comma:b) -> a : split b
                (a, _empty)   -> [a]

5

与其试图修改代码以符合期望,通常更好的做法是先理解代码片段。

split :: String -> [String]
split [] = [""]
split (c:cs) | c == ','  = "" : rest
             | otherwise = (c : head rest) : tail rest
    where rest = split cs

首先让我们分析一下 split 的作用。第一个语句简单地说明了“空字符串的拆分是一个只有一个元素(空字符串)的列表”。这似乎很合理。现在第二个子句陈述道:“如果字符串的开头是逗号,则生成一个列表,其中第一个元素是空字符串,其后是字符串剩余部分的拆分。”最后一个判断说:“如果字符串的第一个字符不是逗号,则把该字符添加到剩余字符串的拆分的第一项之前,然后再加上剩余字符串的其他元素。”请注意,split 返回一个字符串列表,因此 head rest 是一个字符串。

因此,如果我们想要将定界符添加到输出中,则需要将其作为单独的字符串添加到 split 的输出中。在哪里呢?在第一个判断中。我们不应该返回 "," : rest,因为通过递归,头部已经被预置,但是它应该作为一个单独的字符串。所以结果应该是:

split :: String -> [String]
split [] = [""]
split (c:cs) | c == ','  = "" <b>: ","</b> : rest
             | otherwise = (c : head rest) : tail rest
    where rest = split cs

5
那个示例代码的风格不好。除非你确切知道在做什么(这些函数是不安全的,属于偏函数),否则永远不要使用headtail。此外,等式比较通常更好地编写为专用模式。
有了这个想法,示例就变成了:
split :: String -> [String]
split "" = [""]
split (',':cs) = "" : split cs
split (c:cs) = (c:cellCompletion) : otherCells
 where cellCompletion : otherCells = split cs
(严格来说,这仍然不安全,因为匹配cellCompletion:otherCells是不尽如人意的,但至少它发生在一个明确定义的位置,如果出现任何问题将会给出清晰的错误提示。) 现在,在我看来,这让事情变得更加清晰:使用"" : split cs,其目的并不是真的要将一个空单元格添加到结果中。相反,它是要添加一个单元格,该单元格将由递归栈中更高层次的调用填充。这是因为这些调用再次对更深层次的结果进行解构,具有模式匹配cellCompletion: otherCells = split cs,即它们再次弹出第一个单元格,并在前面添加实际的单元格内容。
因此,如果将其更改为",":split,则效果只是您构建的所有单元格已经预先以字符终止了。那不是你想要的。
相反,您想添加一个不再被触摸的额外单元格。那需要在结果中更深的位置:
split (',':cs) = "" : "," : split cs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接