我想要在一个列表中,只替换第一次出现的元素为新值。我写了下面的代码,但使用它会导致所有匹配的元素都被改变。
replaceX :: [Int] -> Int -> Int -> [Int]
replaceX items old new = map check items where
check item | item == old = new
| otherwise = item
我该如何修改代码,以便只有第一个匹配项发生更改?
谢谢您的帮助!
关键在于map
和f
(你示例中的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]
关于这个类型签名,有两件事情需要注意:
由于我们可能会在列表的中间停止应用 f
,所以输入列表和输出列表必须具有相同的类型。(使用map
时,因为将对整个列表进行映射,类型可以改变。)
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
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' [] = []
a
必须是 Eq 类型类的一个实例。这是必需的,以便可以使用 ==
。 - Chris Barrettrep :: 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
map
的好见解,我也同意,但我也不喜欢他的解决方案。Data.List
,函数是break
:
有了这个,我们可以这样解决问题:
break
,应用于谓词p
和列表xs
,返回一个元组,其中第一个元素是xs
中不满足p
的元素的最长前缀(可能为空),第二个元素是列表的其余部分。
old
之前的所有元素和剩下的元素。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]
Data.List
是一个很好的起点。Data.List
的标准函数,并编写自己的版本。编辑: 我刚意识到 break
实际上是 Prelude
函数,不需要导入。 不过,Data.List
是最好的库之一。
使用 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]
>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]]
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
。 - dave4420x
不在xs
中出现,您的安全版本将返回xs ++ [y]
。这不是其他解决方案所做的。我并不是说这是错误的(虽然我认为这不是OP想要的,毫无疑问,在某些情况下,这种行为是正确的),但我认为值得注意。 - dave4420rep xs x y = let (left, right) = break (== x) xs; (yes, no) = splitAt 1 right in left ++ [y | _ <- yes] ++ no
。 - Daniel WagnerreplaceValue :: Int -> Int -> [Int] -> [Int]
replaceValue a b (x:xs)
|(a == x) = [b] ++ xs
|otherwise = [x] ++ replaceValue a b xs
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