编辑列表中每个第N个项目

4

我想对一个整数列表进行算术运算(例如将值翻倍),每隔n个位置。

例如,给定列表[1,2,3,4,5,6,7],我想每三个位置翻倍。在这种情况下,我们会得到[1,2,6,4,5,12,7]

该怎么做呢?

5个回答

13
applyEvery :: Int -> (a -> a) -> [a] -> [a]
applyEvery n f = zipWith ($) (cycle (replicate (n-1) id ++ [f]))
cycle子表达式构建一个函数列表[id,id,...,id,f],具有正确数量的元素,并不断地重复它,而zipWith($)将该函数列表应用于参数列表。

既然你要求更多细节!请随意要求更多解释。

主要思想可能最好用ASCII图来解释(这并不妨碍我写很多ASCII字!):

functions : [ id, id, f  , id, id, f  , id, id, f, ... 
input list: [  1,  2,   3,  4,  5,   6,  7 ]
-----------------------------------------------------
result    : [  1,  2, f 3,  4,  5, f 6,  7 ]
就像没有理由硬编码想要在列表中每隔三个元素就加倍,f(在您的示例中是加倍)也没有什么特殊之处,除了它应该具有与不执行操作相同的结果类型。所以我把这些作为我的函数的参数。甚至操作数字列表都不重要,因此该函数可用于列表 a ,只要给定“间隔”和操作即可。这给我们带来了类型标记applyEvery :: Int -> (a -> a) -> [a] -> [a]。我将输入列表放在最后,因为部分应用程序如doubleEveryThird = applyEvery 3 (*2)是返回新列表的东西,称为组合器。我基本上随机选择了另外两个参数的顺序 :-)
要构建函数列表,我们首先组装基本构建块,由n-1个id组成,后跟一个f,如下所示:replicate (n-1) id ++ [f]replicate m x创建包含m个重复项x的列表,例如replicate 5 'a' = "aaaaa",但它也适用于函数。我们必须附加包含f的自己的列表,而不是使用:,因为您只能在前面添加单个元素-Haskell的列表是单向链接的。
接下来,我们使用cycle(而不是我最初错误地使用的repeat)不断重复基本构建块。 cycle的类型为[a] -> [a],因此结果是“相同级别的嵌套”列表。例如,cycle [1,2,3]计算为[1,2,3,1,2,3,1,2,3,...] [副笔:我们没有使用的唯一反复函数是它本身:repeat形成由其参数组成的无限列表]
解决这些问题后,稍微棘手的部分是zipWith($)部分。您可能已经知道普通的zip 函数,它获取两个列表并将元素放置在结果中的同一位置,当任一列表用尽时终止。画图表现为:
    xs   : [ a  , b    , c   , d, e] 
       ys: [   x,    y ,   z ]
------------------------------
zip xs ys: [(a,x),(b,y),(c,z)]

这已经非常像第一张图片了,对吧?唯一的不同是我们不想将单独的元素组合成一个元组,而是将第一个元素(一个函数)应用于第二个元素。使用自定义组合函数进行zip的操作可以使用zipWith。下面是最后一张图片(我保证!):

          xs   : [   a  ,   b  ,   c   , d, e] 
             ys: [     x,     y,     z ]
----------------------------------------
zipWith f xs ys: [ f a x, f b y, f c z ]

现在,我们应该选择什么与 zipWith 一起使用呢?嗯,我们想要将第一个参数应用于第二个参数,所以 (\f x -> f x) 应该可以胜任。如果 Lambda 表达式让你感到不舒服,你也可以定义一个顶层函数 apply f x = f x 并使用它代替。然而,在 Prelude 中已经有了一个标准运算符,即 $!由于不能将中缀运算符用作独立的函数,我们必须使用语法糖 ($) (其实只是意味着 (\f x -> f $ x)

把以上所有内容综合起来,我们得到:

applyEvery :: Int -> (a -> a) -> [a] -> [a]
applyEvery n f xs = zipWith ($) (cycle (replicate (n-1) id ++ [f])) xs

但我们可以去掉末尾的xs,得到我给出的定义。


2
我可以问一下($)代表什么吗?我认为$只是用来消除括号的。 - MadHatter
2
顺便说一下,你的回答非常优雅和“Haskell风格”.. 我接受Gabriel的回答,因为它可能更适合像我这样的绝对初学者。 但是,如果你愿意,你可以逐步解释你的回答,以进一步帮助任何应该看到这篇文章的初学者。 - MadHatter
1
@MadHatter f $ x = f x - 这意味着 $ 接受一个函数和一个值,并使用该值调用该函数。当作为中缀运算符使用时,$ 的优先级非常低,因此通常用于代替括号。但是它也可以作为普通函数使用,就像这里一样,在这种情况下,它与编写 zipWith (\f x -> f x) (...) 相同。 - amalloy
2
tail . cycle $ f : replicate (n-1) id不是更简洁吗?这样就避免了使用(++ [x])向列表末尾添加元素,这是一个值得避免的好习惯。 - amalloy
我知道在编程中不应该仅仅用注释来表示感谢,但在这种情况下,我感到有必要感谢你们所有人提供的那些有益的解释。 - MadHatter
显示剩余3条评论

9

获取列表中值的索引的常见方法是将列表与 zip 函数一起使用,将其转换为包含 (value, index) 的元组。

ghci > let zipped = zip [1,2,3,4,5,6,7] [1..]
ghci > zipped
[(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7)]

然后,您只需要在该列表上进行map操作并返回一个新列表。如果索引可被3整除(index `rem` 3 == 0),我们将使该值加倍,否则我们将返回相同的值:

ghci > map (\(value, index) -> if index `rem` 3 == 0 then value*2 else value) zipped
[1,2,6,4,5,12,7]

Tell me if that all makes sense—I can add more detail if you aren't familiar with zip and map and such.

Zip

你可以通过查看其Haddocks找到有关zip的文档,其中写道:"zip接受两个列表并返回一个对应的元素对列表。"(文档托管在多个地方,但我去了https://www.stackage.org并搜索了zip)。

Map

map函数将一个函数应用于列表中的每个项目,为每个元素生成一个新值。

Lambdas

lambda函数是没有特定名称的函数。我们在map的第一个参数中使用了一个lambda函数,以说明我们应该对列表中的每个元素执行什么操作。您可能已经在其他编程语言(如Python、Ruby或Swift)中看到过这些。

这是lambda函数的语法:

(\arg1, arg2 -> functionBodyHere)

我们也可以不使用Lambda表达式来编写它:
ghci > let myCalculation (value, index) = if index `rem` 3 == 0 then value*2 else value
ghci > map myCalculation zipped
[1,2,6,4,5,12,7]

是的,请添加更多细节,如果您愿意。我是Haskell的绝对新手。谢谢! - MadHatter
我认为yatima的答案更加优雅,如果我可以这么说的话。但是你的回答对我这个绝对的初学者来说更有教育意义。我会接受你的回答。 - MadHatter
谢谢你对lambda的解释,非常有用。 让我再问你一个问题:为什么你写了let mycalculation而不是定义一个带类型声明等等的适当函数呢? - MadHatter
@MadHatter 我是在 GHCi 中编写这个函数的,在 GHCi 中你需要使用 let 来定义函数(在 GHCi 中运行的代码就像添加到 main 函数中的代码,而不是函数的顶层定义)。如果我是在文件中编写代码,我会有类型声明等内容。 - MaxGabriel

3
注意:这段代码尚未经过测试。
在镜头(lens)的领域中,这被称为“遍历”(Traversal)。而Control.Lens库则提供了这些内容:
{-# LANGUAGE RankNTypes, ScopedTypeVariables #-}

type Traversal s t a b =
  forall f . Applicative f => (a -> f b) -> s -> f t

type Traversal' s a = Traversal s s a a

我们可以使用来自Control.Lens.Indexedlensitraverse
-- everyNth :: (TraversableWithIndex i t, Integral i)
            => i -> Traversal' (t a) a
everyNth :: (TraversableWithIndex i t, Integral i, Applicative f)
         => i -> (a -> f a) -> t a -> f (t a)
everyNth n f = itraverse f where
  g i x | i `rem` n == n - 1 = f x
        | otherwise = pure x

这可以根据您的具体目的进行专门定制:
import Data.Profunctor.Unsafe
import Data.Functor.Identity

everyNthPureList :: Int -> (a -> a) -> [a] -> [a]
everyNthPureList n f = runIdentity #. everyNth n (Identity #. f)

2
mapIf :: (Int -> Bool) -> (a -> a) -> [a] -> [a]
mapIf pred f l = map (\(value,index) -> if (pred index) then f value else value) $ zip l [1..]

mapEveryN :: Int -> (a -> a) -> [a] -> [a]
mapEveryN n = mapIf (\x -> x `mod` n == 0)

在 Ideone 上实时运行


1
一个简单的递归方法:

everyNth n f xs = igo n xs where
    igo 1 (y:ys) = f y : igo n     ys
    igo m (y:ys) =   y : igo (m-1) ys
    igo _ [] = []

doubleEveryThird = everyNth 3 (*2)

基本上,igo 从 n 开始倒数,直到达到 1,然后应用函数,并返回到 ndoubleEveryThird 是部分应用的: everyNth 需要三个参数,但我们只给了两个,所以 dougleEveryThird 将期望最后一个参数。

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