【问题标题】:Split String into List of Strings of given length using fold使用折叠将字符串拆分为给定长度的字符串列表
【发布时间】:2021-06-25 20:58:36
【问题描述】:

我想使用 fold 将字符串拆分为给定长度的字符串列表。

split :: n -> String -> [String]
split 4 "abcdefghijklmnopqrst" -> ["abcd","efgh","ijkl","mnop","qrst"]

这可以使用foldr 完成吗?您能否提供一个解释,以便我对如何使用折叠来解决此类问题有一些直觉?

编辑:我添加了递归解决方案

split :: Int -> String -> [String]
split len xs
  | len >= length xs = [xs]
  | otherwise = take len xs : split len (drop len xs)

【问题讨论】:

  • 你尝试了什么,什么不起作用?
  • 这种递归函数对于转换为折叠可能不太有用。您需要一个一个地获取列表元素的自然递归。这通常涉及到 []x:xs 之类的模式匹配。
  • 虽然我同意@n.1.8e9-where's-my-sharem.,但我确实认为这种问题是一个很好的练习。
  • 框架挑战:为什么你想使用foldr?没有经验丰富的 Haskell 程序员会这样做,即使在他们不只是导入执行此操作的函数的反事实世界中也是如此。
  • @danielwagner 我只是想用 foldr 来做这件事,因为我是 Haskell 的新手,想了解更多折叠的真正工作原理。没有必要使用折叠来做到这一点,因为这是我需要解决一个更大的问题的功能,这是我试图解决的练习的主要目标。我只是好奇使用 foldr 的解决方案是什么。

标签: haskell fold


【解决方案1】:

要从您的递归代码转到foldr,您需要进一步内联并融合lengthtakedrop 的定义,以便您的代码一次解构输入列表一个元素,例如@ 987654326@ 确实:

split :: Int -> [a] -> [[a]]
split len xs
  | len >= length xs = [xs]
  | otherwise = take len xs : split len (drop len xs)
  where
  take :: Int -> [a] -> [a]
  take 0 xs = xs
  take 0 [] = []
  take n (x:xs) = x : take (n-1) xs
  -- and for `drop` and `len >= length` as well
  -- ......

所以我们需要为此添加一个计数器。

split2 :: Int -> [a] -> [[a]]
split2 len xs = go xs 0
  where
  go [] _  = [[]]    -- [] : []
  go (x:xs) i 
    | i < len = [x] : go xs (i+1)
    | otherwise =     go xs 0

(请在此处修复一个错误,好吗)。

这当然不会产生正确的结果,但它已经遵循foldr 模式!它同时进行计数和跳过,并正确选择从0 重新开始计数的点,这与dropped 列表的递归调用相对应——已经免费完成,只需沿着input 一次列出一个元素——就像foldr 一样。我们只需要保存我们目前忘记的那些元素:

split3 :: Int -> [a] -> [[a]]
split3 len xs = go xs 0 []
  where
  go [] _ ys = [reverse ys]
  go (x:xs) i ys
    | i < len = go xs (i+1) (x:ys)
    | otherwise = reverse (x:ys) : go xs 0 []

这直接映射到foldr 递归模式,我们将LHS 上的go (x:xs) 替换为g x r,将RHS 上的go xs 替换为r

split4 :: Int -> [a] -> [[a]]
split4 len xs = foldr g z xs 0 []
  where
  z _ ys = [reverse ys]
  g x r i ys 
    | i < len = r (i+1) (x:ys)
    | otherwise = reverse (x:ys) : r 0 []

-- > split4 4 "123456"
-- ["12345","6"]

再次,请务必修复一个错误。

不过,这并不完全是惰性的,即 (!! 0) . head . split4 3 $ "12"++undefined 会发散而不是返回字符 '1',因为它可以说是应该的。

为了实现,我们必须立即返回结构,即使其中有洞,并在以后填补这些洞:

split5 :: Int -> [a] -> [[a]]
split5 len xs = foldr g z xs 1
  where
  z _ = [[]]
  g x r i
    | i < len = let (a:b) = r (i+1) in (x:a):b
    | otherwise = [x] : r 1

有了这个,

> split5 4 "123456"
["1234","56"]

> take 2 . head . split5 4 $ "12" ++ undefined
"12"

这里仍然存在返回值不太正确的极端情况。一定要找到它,并尝试修复它。

【讨论】:

  • 非常好的方法!为了解决尾随[] 的问题,我们可以让z 在其参数为1 时返回[]。但是,当len0 时,它的行为仍然与 OP 的递归解决方案(以及我的变形解决方案)不同。关于在这种情况下如何/是否可以像我的解决方案一样工作的任何想法,而不单独处理它(并写repeat [])?
  • @peterpun 您提到的这个解决方案在哪里发布?一般来说,如果一些特殊情况需要一些特殊处理,而一般代码不能很好地处理,我想我们会通过一些前/后/处理步骤来解决它。
  • *通过----当然这个问题很适合展开器,正如 cmets 中所提到的。通过 foldr 让它工作只是一个练习。
  • 对不起,它在问题的 cmets 中。我只是好奇我们是否可以用变质完全模拟这种变形。当然,len0 的情况很特殊,因为这样各部分不能同时具有最多 len 的长度,并且在连接时产生输入(当输入非空时)。
  • 有一个问题,关于在某些类似情况下如何不可避免地进行后/预处理/处理步骤,我认为,也涉及 foldr、IIRC,由 chi 回答。 here it is(看看是否相关:))
【解决方案2】:

正如其他人评论的那样,使用 fold 可能不是最好的方法。然而,这是一个有趣的练习。

foldr 的签名我们可以看出,累加器需要是[String] 类型和Char -&gt; [String] -&gt; [String] 类型的组合运算符。

要实现combine,请注意我们可以将一个字符一个字符地附加到累加器中。在此过程中,我们需要注意每次超过n 个字符时都从一个新字符串开始:

split :: Int -> String -> [String]
split n = foldr combine []
    where
        combine c (x:xs) | length x < n = (c:x):xs
        combine c xs = [c]:xs

【讨论】:

  • 这不是懒惰的,不适用于无限列表;另外,split 4 "123456" == ["12","3456"] 而不是由 OP 的递归代码生成的 ["1234","56"]
猜你喜欢
  • 1970-01-01
  • 2012-11-20
  • 1970-01-01
  • 2012-07-22
  • 1970-01-01
  • 2015-02-13
  • 2023-03-11
  • 2019-04-16
相关资源
最近更新 更多