我想对一个整数列表进行算术运算(例如将值翻倍),每隔n个位置。
例如,给定列表[1,2,3,4,5,6,7]
,我想每三个位置翻倍。在这种情况下,我们会得到[1,2,6,4,5,12,7]
。
该怎么做呢?
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)
是返回新列表的东西,称为组合器。我基本上随机选择了另外两个参数的顺序 :-)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
,得到我给出的定义。
获取列表中值的索引的常见方法是将列表与 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]
zip
and map
and such.
你可以通过查看其Haddocks找到有关zip
的文档,其中写道:"zip接受两个列表并返回一个对应的元素对列表。"(文档托管在多个地方,但我去了https://www.stackage.org并搜索了zip
)。
map
函数将一个函数应用于列表中的每个项目,为每个元素生成一个新值。
lambda函数是没有特定名称的函数。我们在map
的第一个参数中使用了一个lambda函数,以说明我们应该对列表中的每个元素执行什么操作。您可能已经在其他编程语言(如Python、Ruby或Swift)中看到过这些。
这是lambda函数的语法:
(\arg1, arg2 -> functionBodyHere)
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]
let mycalculation
而不是定义一个带类型声明等等的适当函数呢? - MadHatterlet
来定义函数(在 GHCi 中运行的代码就像添加到 main
函数中的代码,而不是函数的顶层定义)。如果我是在文件中编写代码,我会有类型声明等内容。 - MaxGabriel{-# 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.Indexed
的lens
的itraverse
:-- 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)
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)
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)
n
开始倒数,直到达到 1
,然后应用函数,并返回到 n
。 doubleEveryThird
是部分应用的: everyNth
需要三个参数,但我们只给了两个,所以 dougleEveryThird
将期望最后一个参数。
($)
代表什么吗?我认为$
只是用来消除括号的。 - MadHatterf $ x = f x
- 这意味着$
接受一个函数和一个值,并使用该值调用该函数。当作为中缀运算符使用时,$
的优先级非常低,因此通常用于代替括号。但是它也可以作为普通函数使用,就像这里一样,在这种情况下,它与编写zipWith (\f x -> f x) (...)
相同。 - amalloytail . cycle $ f : replicate (n-1) id
不是更简洁吗?这样就避免了使用(++ [x])
向列表末尾添加元素,这是一个值得避免的好习惯。 - amalloy