在Haskell中仅替换列表中的一个元素

6

我想要在一个列表中,只替换第一次出现的元素为新值。我写了下面的代码,但使用它会导致所有匹配的元素都被改变。

replaceX :: [Int] -> Int -> Int -> [Int]
replaceX items old new = map check items where
check item  | item == old = new 
            | otherwise = item

我该如何修改代码,以便只有第一个匹配项发生更改?

谢谢您的帮助!

8个回答

11

关键在于mapf(你示例中的check)只相互通信如何转换单个元素。它们不会相互通信有多远需要转换元素:map总是一直处理到列表末尾。

map :: (a -> b) -> [a] -> [b]
map _ []     = []
map f (x:xs) = f x : map f xs

让我们写一个新版本的map --- 我会叫它mapOnce,因为我想不出更好的名字。

mapOnce :: (a -> Maybe a) -> [a] -> [a]

关于这个类型签名,有两件事情需要注意:

  1. 由于我们可能会在列表的中间停止应用 f,所以输入列表和输出列表必须具有相同的类型。(使用map时,因为将对整个列表进行映射,类型可以改变。)

  2. f 的类型没有改变为 a -> a,而是变成了 a -> Maybe a

    • Nothing 表示“保持此元素不变,继续向下处理列表”
    • Just y 表示“更改此元素,并保持其余元素不变”

因此:

mapOnce _ []     = []
mapOnce f (x:xs) = case f x of
        Nothing -> x : mapOnce f xs
        Just y  -> y : xs

你的示例现在为:

replaceX :: [Int] -> Int -> Int -> [Int]
replaceX items old new = mapOnce check items where
    check item  | item == old = Just new 
                | otherwise   = Nothing

1
非常感谢您的精确解释!我学到了很多。 - Afflatus

4
你可以轻松地将其写成递归迭代,如下所示:
rep :: Eq a => [a] -> a -> a -> [a]
rep items old new = rep' items
    where rep' (x:xs) | x == old  = new : xs
                      | otherwise = x : rep' xs
          rep' [] = []

我对Haskell还不太熟悉,请问"Eq a=>"是什么意思? - Afflatus
这是一种类型约束;它意味着 a 必须是 Eq 类型类的一个实例。这是必需的,以便可以使用 == - Chris Barrett

4
一种直接的实现方式是:
rep :: Eq a => a -> a -> [a] -> [a]
rep _ _ [] = []
rep a b (x:xs) = if x == a then b:xs else x:rep a b xs

我喜欢将列表作为最后一个参数来执行类似以下的操作:
myRep = rep 3 5 . rep 7 8 . rep 9 1

3
坦白地说,到目前为止我不喜欢大部分的答案。dave4420提供了一些关于map的好见解,我也同意,但我也不喜欢他的解决方案。
为什么我不喜欢这些答案呢?因为你应该学会将这些问题分解成更小的问题,可以通过更简单的函数来解决,最好是库函数。在这种情况下,库是Data.List,函数是break

break,应用于谓词p和列表xs,返回一个元组,其中第一个元素是xs中不满足p的元素的最长前缀(可能为空),第二个元素是列表的其余部分。

有了这个,我们可以这样解决问题:
  1. 将列表分成两部分:第一个出现 old 之前的所有元素和剩下的元素。
  2. "剩下" 的列表要么为空,要么其第一个元素是 old 的第一次出现。这两种情况都很容易处理。

所以我们有了这个解决方案:

import Data.List (break)

replaceX :: Eq a => a -> a -> [a] -> [a] 
replaceX old new xs = beforeOld ++ replaceFirst oldAndRest 
    where (beforeOld, oldAndRest) = break (==old) xs
          replaceFirst [] = []
          replaceFirst (_:rest) = new:rest

例子:

*Main> replaceX 5 7 ([1..7] ++ [1..7])
[1,2,3,4,7,6,7,1,2,3,4,5,6,7]

所以我的建议是:
  1. 学习如何导入库。
  2. 研究库文档并学习标准函数。Data.List 是一个很好的起点。
  3. 尽可能多地使用这些库函数。
  4. 作为自学练习,您可以选择一些来自 Data.List 的标准函数,并编写自己的版本。
  5. 当遇到无法通过库函数组合解决的问题时,请尝试发明自己的通用函数。

编辑: 我刚意识到 break 实际上是 Prelude 函数,不需要导入。 不过,Data.List 是最好的库之一。


3

使用 Lens库 的另一种方法。

>import Control.Lens
>import Control.Applicative

>_find :: (a -> Bool) -> Simple Traversal [a] a                                   
>_find _ _ [] = pure []                                                           
>_find pred f (a:as) = if pred a                                                  
>                       then (: as) <$> f a                                       
>                       else (a:) <$> (_find pred f as)

这个函数接受一个 (a -> Bool) 的函数作为参数,该函数返回值为 True 表示需要修改的类型 'a'。

如果需要将第一个大于 5 的数字加倍,则可以编写如下代码:

>over (_find (>5)) (*2) [4, 5, 3, 2, 20, 0, 8]
[4,5,3,2,40,0,8]

镜头的好处在于可以通过组合它们来进行合成。因此,如果我们想要将第二个子列表中小于100的第一个数字归零,我们可以使用以下方法:
>over ((element 1).(_find (<100))) (const 0) [[1,2,99],[101,456,50,80,4],[1,2,3,4]]
[[1,2,99],[101,456,0,80,4],[1,2,3,4]]

2
也许不是最快的解决方案,但易于理解:
rep xs x y = 
  let (left, (_ : right)) = break (== x) xs 
  in left ++ [y] ++ right 

[编辑]

正如Dave所评论的,如果x不在列表中,这将失败。一个安全的版本应该是:

rep xs x y = 
  let (left, right) = break (== x) xs 
  in left ++ [y] ++ drop 1 right 

[编辑]

啊啊啊!!!

rep xs x y = left ++ r right where
  (left, right) = break (== x) xs 
  r (_:rs) = y:rs
  r [] = []

尽管模式匹配将失败,如果xs中没有出现x - dave4420
关于您的编辑:如果x不在xs中出现,您的安全版本将返回xs ++ [y]。这不是其他解决方案所做的。我并不是说这是错误的(虽然我认为这不是OP想要的,毫无疑问,在某些情况下,这种行为是正确的),但我认为值得注意。 - dave4420
这是一个技巧:rep xs x y = let (left, right) = break (== x) xs; (yes, no) = splitAt 1 right in left ++ [y | _ <- yes] ++ no - Daniel Wagner

1
replaceValue :: Int -> Int -> [Int] -> [Int]
replaceValue a b (x:xs) 
        |(a == x) = [b] ++ xs
        |otherwise = [x] ++ replaceValue a b xs

0
这是一种命令式的方法,使用 State Monad:
import Control.Monad.State                                                  

replaceOnce :: Eq a => a -> a -> [a] -> [a]
replaceOnce old new items = flip evalState False $ do
  forM items $ \item -> do
    replacedBefore <- get
    if item == old && not replacedBefore
      then do
        put True
        return new
      else
        return old

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