【发布时间】:2021-12-25 04:21:38
【问题描述】:
我想对整数列表每隔 n 个位置执行一次算术运算(例如,将值加倍)。
例如,给定列表[1,2,3,4,5,6,7],我想每三个位置将值加倍。在这种情况下,我们将拥有[1,2,6,4,5,12,7]。
我该怎么做?
【问题讨论】:
我想对整数列表每隔 n 个位置执行一次算术运算(例如,将值加倍)。
例如,给定列表[1,2,3,4,5,6,7],我想每三个位置将值加倍。在这种情况下,我们将拥有[1,2,6,4,5,12,7]。
我该怎么做?
【问题讨论】:
一个简单的递归方法:
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,它会应用函数,然后回到n。 doubleEveryThird 被部分应用:everyNth 需要三个参数,但我们只给了它两个,所以dougleEveryThird 将需要最后一个参数。
【讨论】:
注意:此代码尚未测试。
在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.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)
【讨论】:
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) 这样的部分应用程序会返回一个新列表,即所谓的combinator。我基本上随机选择了其他两个参数的顺序:-)
为了构建函数列表,我们首先组装基本构建块,由 n-1 个ids 和一个f 组成,如下所示:replicate (n-1) id ++ [f]。 replicate m x 创建一个列表,其中包含 m 重复的 xargument,例如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-y函数是repeat本身:它形成了一个由其参数组成的无限列表]
除此之外,有点棘手的zipWith ($) 部分。您可能已经知道普通的 zip 函数,它接受两个列表并将元素放在结果中的元组中的相同位置,当任一列表用完元素时终止。图示:
xs : [ a , b , c , d, e]
ys: [ x, y , z ]
------------------------------
zip xs ys: [(a,x),(b,y),(c,z)]
这已经很像第一张照片了,对吧?唯一的问题是我们不想将单个元素放在一个元组中,而是将第一个元素(它是一个函数)应用于第二个元素。使用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,得到我给出的定义。
【讨论】:
($) 代表什么?我认为 $ 只是为了摆脱括号很有用..
f $ x = f x - 即$ 接受一个函数和一个值,并用该值调用函数。它经常用于代替括号,因为$ 在用作中缀运算符时具有非常低的优先级。但是它可以作为一个普通函数使用,就像这里一样,在那种情况下它和写zipWith (\f x -> f x) (...)是一样的。
tail . cycle $ f : replicate (n-1) id而不是cycle ((replicate (n-1) id) ++ [f])不是更干净吗?您避免必须使用 (++ [x]) 来添加到列表的末尾,这是避免的一件好事。
concat . intersperse f $ replicate (n-1) id 也可以解决问题,现在我想到了,但这需要Data.List。
获取列表中值的索引的常用方法是将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 并返回一个新列表。如果 index 可以被 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 和 map 等,我可以添加更多细节。
您可以通过查看 Haddocks 找到有关 zip 的文档,其中说:“zip 接受两个列表并返回对应对的列表。” (文档托管在几个地方,但我去了https://www.stackage.org并搜索了zip)。
map 函数将函数应用于列表中的每个项目,为每个元素生成一个新值。
Lambda 只是没有特定名称的函数。我们在map 的第一个参数中使用了一个来说明我们应该对列表中的每个元素做什么。您可能已经在 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]
【讨论】:
let mycalculation 而不是用类型声明等定义一个适当的函数?
let 的函数(您在 GHCi 中运行的代码就像您添加到 main 函数中的代码,而不是就像函数的顶级定义)。如果我在文件中编写代码,我会有一个类型声明等。
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)
【讨论】: