【问题标题】:Remove first element that fulfills predicate (Haskell)删除满足谓词的第一个元素(Haskell)
【发布时间】:2021-09-25 12:12:32
【问题描述】:

我想创建一个函数来删除满足第二个参数中给出的谓词的第一个元素。像这样的:

removeFirst "abab" (< 'b')  = "bab"
removeFirst "abab" (== 'b') = "aab"
removeFirst "abab" (> 'b')  = "abab"
removeFirst [1,2,3,4] even  = [1,3,4]

我想通过递归来做到这一点,并想出了这个:

removeFirst :: [a] -> (a -> Bool) -> [a]
removeFirst [] _ = []
rremoveFirst (x:xs) p = if p x then x : removeFirst xs p else removeFirst xs p

(灵感来自this问题) 但我收到一个类型错误,如下所示:

Couldn't match type ‘a’ with ‘Bool’
  Expected: [Bool]
    Actual: [a]
  ‘a’ is a rigid type variable bound by
    the type signature for:
      removeFirst :: forall a. [a] -> (a -> Bool) -> [a]

或者这个:

ghci> removeFirst [1,2,3,4] even

<interactive>:25:1: error:
    * Variable not in scope: removeFirst :: [a0] -> (a1 -> Bool) -> t
    * Perhaps you meant `rem' (imported from Prelude)

我知道这是一个相对简单的编程,只是我对 Haskell 还不够熟悉。我怎样才能做到这种“Haskell 风格”(一行)?

【问题讨论】:

  • 无法重现(即使在修正了拼写错误 rremoveFirst -> removeFirst 之后)。你确定你复制了你正在尝试的代码完全?特别是,您的原始代码在then 分支中是否有p x 而不是x

标签: haskell


【解决方案1】:

在“按风格”做之前,为什么不先简单地它,让它工作。这就是我们学习的方式。

"Variable not in scope: removeFirst ..." 仅表示您尚未定义名为removeFirst 的函数。

看来您首先尝试定义它(并且您显示的错误与您显示的代码一致),然后出现错误,因此没有定义,然后您尝试调用它并得到错误说它尚未定义,自然。

因此,将您的程序保存在源文件中,然后在 GHCi 中加载该文件。然后,如果您遇到任何错误,请将文件中的完整代码复制粘贴到您的问题中(不要手动重新输入)。另外,准确地,请具体说明您收到错误消息时的操作。并确保通过复制粘贴完整地包含错误消息。

那么你的代码的逻辑就可以解决了。


由于其他人已经发布了工作代码,因此我将其编码为一种单行代码:

remFirst :: [a] -> (a -> Bool) -> [a]
remFirst xs p = foldr g z xs xs
  where
  g x r ~(_:tl)      -- "r" for recursive result
     | p x           -- we've found it, then
       = tl          -- just return the tail
     | otherwise
       = x : r tl    -- keep x and continue
  z _  = []          -- none were found

缩短,变成

remFirst xs p = 
  foldr (\x r ~(_:tl) -> if p x then tl else x : r tl)
        (const []) xs xs

【讨论】:

  • (将答案发布为答案而不是评论。)
  • 感谢您的回复。我正在尝试边做边学,但我不只是了解背后的理论和逻辑,而是边做边学。所以我正在尝试做“简单”的任务,然后边走边学。我非常感谢建设性的反馈,我不会手动在堆栈中重新键入代码。
  • 正如我在答案中所说,错误消息与您显示的代码不对应,因此您必须加载了一些其他代码。你是从文件中加载的,还是在提示符下输入? (您在其中一行中也有错字,rremoveFirst...FYI)。
  • SO 方式是,按原样发布特定代码、其测试调用和错误消息,并获得答案。 :) 这不是一个您可以自行获取线索并解决问题的论坛。问答条目应该是一致的,以讲述一个特定的,如果非常简短和重点突出的故事。不要对其他地方正在发生的事情发表评论。 :) 快乐的小径。 :)
  • @Mampenda 顺便说一句,这根本不是批评,只是一些建议,以便未来的合作对我们所有人都更有成效。
【解决方案2】:

不是一行,但它有效。

 removeFirst :: [a] -> (a -> Bool) -> [a]
 removeFirst (x:xs) pred
   | pred x    = xs
   | otherwise = x : removeFirst xs pred

对于单行,我想您可能想使用 foldl 从左侧遍历列表。

编辑

这个解决方案使用守卫,它首先检查传入的列表的第一个元素是否满足谓词,如果不满足,则将其添加到列表的前面并递归检查传入列表的尾部。

【讨论】:

  • 请说明你的答案,并说明问题中遇到的问题。
  • OP 没有要求解释,只是一个解决方案。
  • 您可以将 pred x == True 替换为 pred x。这已经是一个布尔值了。
  • @jpmarinier 注意。
【解决方案3】:

使用手动递归不会导致单线解决方案,所以让我们尝试使用库中的一些预构建递归方案。

函数scanl :: (b -&gt; a -&gt; b) -&gt; b -&gt; [a] -&gt; [b] 看起来很方便。它产生一连串状态,每个输入项一个状态。

ghci解释器下测试:

$ ghci
 λ> 
 λ> p = (=='b')
 λ> 
 λ> xs = "ababcdab"
 λ> ss = tail $ scanl (\(s,n) x -> if (p x) then (x,n+1) else (x,n)) (undefined,0)  xs
 λ> 
 λ> ss
 [('a',0),('b',1),('a',1),('b',2),('c',2),('d',2),('a',2),('b',3)]
 λ> 

此时,通过一些简单的数据按摩,很容易发现并摆脱一个不需要的元素:

 λ> 
 λ> filter (\(x,n) -> (n /= 1) || (not $ p x))  ss 
 [('a',0),('a',1),('b',2),('c',2),('d',2),('a',2),('b',3)]
 λ> 
 λ> map fst $ filter (\(x,n) -> (n /= 1) || (not $ p x))  ss 
 "aabcdab"
 λ> 

现在让我们编写removeFirst 函数。我冒昧地将谓词作为最左边的参数;这就是所有库函数的作用。

removeFirst :: (a -> Bool) -> [a] -> [a]
removeFirst p =
    let
        stepFn = \(s,n) x -> if (p x) then (x,n+1) else (x,n)
        p2     = \(x,n) -> (n /= 1) || (not $ p x)
    in
        map fst . filter p2 . tail . scanl stepFn (undefined,0)

如果需要,可以将此版本更改为单行解决方案,只需将stepFnp2 的值扩展到最后一行。留给读者作为练习。它会排长队,因此是否能提高可读性还有待商榷。

附录:

另一种方法是尝试查找库函数,类似于splitAt :: Int -&gt; [a] -&gt; ([a], [a]),但采用谓词而不是列表位置。

所以我们将(a -&gt; Bool) -&gt; [a] -&gt; ([a],[a]) 类型签名提交到Hoogle 专用搜索引擎。

这很容易找到break 库函数。这正是我们所需要的。

 λ> 
 λ> break (=='b') "zqababcdefab"
 ("zqa","babcdefab")
 λ> 

所以我们可以这样写我们的removeFirst函数:

removeFirst :: (a -> Bool) -> [a] -> [a]
removeFirst p xs = let  (ys,zs) = break p xs  in  ys ++ (tail zs)

source code for break 只是使用手动递归。

【讨论】:

  • 我认为这根本没有争议。 :) 这段代码确实有点too 笨拙。它打破了良好的最小(即高效)代码的主要原则之一,通过在事先知道这些结果时构造稍后要测试的值。实现过滤器的标准方法是使用 foldr;为了制作一个可以在中间停止的文件夹,我们向它传递另一个参数;为了让组合函数访问当前的尾部(a.o.t. 只有当前的头部,如 foldr),我们向它传递另一个参数,它是列表本身,(续)
  • ...advancing along it by hand,移动了一个位置(可能;也可能不是;根据需要)。 (有些人认为这个方案是"disgusting" :))这将使它(an emulation of)成为一个变形。最终的代码也应该足够短。 :)
  • 我忍不住,在我的答案中添加了我想到的代码。它实际上也是自己写的。 :)
  • @WillNess 是的,我明白了,非常惯用的解决方案。如果我理解正确的话,有点类似于 foldl 使用 foldr 的表达。
  • 那是\p -&gt; ((++) . fst &lt;*&gt; tail . snd) . break p。 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-12
  • 2015-08-25
  • 2011-11-09
  • 1970-01-01
  • 1970-01-01
  • 2018-04-23
相关资源
最近更新 更多