映射函数应用于列表的特定元素

Map function applied to specific elements of the list

我有一个功能:

mapAtOriginal :: (a -> a) -> [Int] -> [a] -> [a]
mapAtOriginal f is xs = helper 0 is xs
  where
    helper _ [] xs = xs
    helper c (i:is) (x:xs)
      | c < i     =   x : helper (c+1) (i:is) xs
      | otherwise = f x : helper (c+1)    is  xs

它是这样工作的:

mapAtOriginal (*2) [0,3] [1,2,3,4]   -- == [2,2,3,8]

所以我想重写它,但是使用 map 函数。我知道 map 适用于列表的每个元素,但是,我需要它只适用于特定索引。

我该怎么做?

map 不知道它在“列表中的什么位置”。因此,您首先需要将该信息编码到元素本身中。这可以用 zip [0..] 来完成,它基本上用每个元素出现的位置来注释它。

然后,在你map的函数中,你只需要对注释元组进行模式匹配,并使用if来决定是否将操纵器函数应用于其他元组元素。

请注意,zipmap 的组合始终等同于单次传递 zipWith,因此您应该优先使用它。

如果您坚持使用 map 来解决这个问题,一个可能的途径是使用类型为 [(a, Bool)] 的辅助列表,其中只有索引元素与 True 布尔值。

生成这个辅助列表的函数可以这样声明:

markIndexedElements :: [Int] -> [a] -> [(a, Bool)]

如果我们有这样一个函数,剩下的问题就变得简单了:

 λ> auxList = markIndexedElements [0,3] [1,2,3,4]
 λ> auxList
 [(1,True),(2,False),(3,False),(4,True)]
 λ> 
 λ> map  (\(x, b) -> if b then (2*x) else x)  auxList
 [2,2,3,8]
 λ> 

markIndexedElements 函数从第一个列表中生成第二个列表,同时保留一些 state 信息。所以如果我们更喜欢一个现成的递归方案,这似乎是scanl :: (b -> a -> b) -> b -> [a] -> [b]函数的工作。

要维护的状态主要由原始列表中的当前位置加上到目前为止未使用的索引列表组成。如果当前位置等于下一个索引,我们将删除该索引并输出一个 (x, True) 对。这给出了以下代码:

-- only indexed elements to get paired with a True value
markIndexedElements :: [Int] -> [a] -> [(a, Bool)]
markIndexedElements indices xs =
    let  sfn ((_,p),(pos,ind)) y =  -- stepping function for scanl
             if (null ind)
               then ((y, False), (pos+1,[]))
               else ((y, pos==head ind),
                     (pos+1, if (pos==head ind)  then  tail ind  else  ind))
         scanRes = scanl  sfn  ((undefined,False), (0,indices))  xs
    in   map fst  $  drop 1 scanRes

剩下的代码不难写:

mapAtOriginal :: (a -> a) -> [Int] -> [a] -> [a]
mapAtOriginal fn indices xs =
    let  pairList  = markIndexedElements indices xs   -- auxiliary list
         pfn (x,b) = if b then (fn x) else x          -- auxiliary function
    in   map pfn pairList


main = do
    let  xs      = [1,2,3,4]
         indices = [0,3]
         result  = mapAtOriginal (*2) indices xs
    putStrLn $ show (markIndexedElements indices xs)
    putStrLn $ show result

程序输出:

[(1,True),(2,False),(3,False),(4,True)]
[2,2,3,8]

类似的解决方案:

如果我们尝试对这个问题再应用一个笛卡尔还原论步骤,我们可以注意到参数 xsmarkIndexedElements 函数中只起次要作用。一种可能性是完全消除它,取而代之的是 returns 只是一个无限的布尔值列表的函数:

booleansFromIndices :: [Int] -> [Bool]
booleansFromIndices indices =
    let  sfn (pos,ind) =  Just $  -- stepping function for unfoldr
             if (null ind)
               then ( False, (pos+1, []) )
               else ( pos==head ind,
                      (pos+1, if (pos==head ind)  then  tail ind  else  ind))
    in   unfoldr  sfn  (0,indices)

结果列表在最后一个索引之后以 False 个值的无限序列结束:

 λ> take 10 $ booleansFromIndices [0,3]
 [True,False,False,True,False,False,False,False,False,False]
 λ> 

然后可以使用 booleansFromIndices:

重写目标 mapAtOriginal 函数
mapAtOriginal :: (a -> a) -> [Int] -> [a] -> [a]
mapAtOriginal fn indices xs =
    let  pairList   = zip xs $ booleansFromIndices indices
         pfn (x, b) = if b  then  (fn x)  else  x
    in   map pfn pairList

最后但同样重要的是,正如其他 answerers/commenters 已经指出的,map+zip 方案通常可以替换为基于 zipWith 功能的方案。 像这样,在我们的例子中:

mapAtOriginal :: (a -> a) -> [Int] -> [a] -> [a]
mapAtOriginal fn indices xs =
    let  booleans = booleansFromIndices indices
    in   zipWith  (\x b -> if b  then  (fn x)  else  x)  xs  booleans

jpmarinier's 的想法出发,填补索引列表中的空白;使用包 data-ordlist

{-# LANGUAGE TupleSections #-}

import qualified Data.List.Ordered    as   O
import           Data.Ord  (comparing)

mapAtOriginal :: (a -> a) -> [Int] -> [a] -> [a]
mapAtOriginal f is xs = 
  zipWith (flip ($))                        -- apply zippily
    xs
    (map fst $                              -- recover the functions, `f` or `id`
          O.unionBy (comparing snd)         -- `is` is assumed subset of [0..]
                    (map (f  ,) is   )      -- tag with
                    (map (id ,) [0..]) )    --      indices

这就像 getZipList $ (f `_at` is) `_or` (id `_at` [0..]) <*> ZipList xs_at_or 的一些临时定义。

这利用了 data-ordlistunion 是左偏的事实,即它在碰撞时从第一个参数列表中选择元素而不是第二个参数列表中的元素。