在纯函数式编程语言中(如Haskell),或者只是以函数方式使用它(例如clojure);假设你有一个整数的list / seq / enumerable(未知大小),你想要生成一个包含连续项之间差异的新list / seq / enumerable,你会怎么做?
我之前在C#中所做的是折叠列表并将状态对象保留为记录“上一个”项目的聚合值,以便您可以从当前项目对其进行差异处理。结果列表也必须进入状态对象(这对于未知大小的列表来说是一个问题)
在功能上做这种事情的一般方法是什么?
答案 0 :(得分:32)
在Haskell中你可能只使用一些更高阶函数,如zipWith
。所以你可以这样做:
diff [] = []
diff ls = zipWith (-) (tail ls) ls
请注意我是如何单独处理[]
案例的 - 如果将空列表传递给tail
则会出现运行时错误,而Haskellers 确实, 讨厌运行时错误。但是,在我的函数中,我保证ls
不为空,因此使用tail
是安全的。 (作为参考,tail
只返回除列表第一项以外的所有内容。它与Scheme中的cdr
相同。)
这只是获取列表及其尾部,并使用(-)
函数组合所有项目。
给定一个列表[1,2,3,4]
,这将是这样的:
zipWith (-) [2,3,4] [1,2,3,4]
[2-1, 3-2, 4-3]
[1,1,1]
这是一种常见的模式:通过巧妙地使用标准的高阶函数,您可以计算出惊人的很多东西。你也不怕把一个列表和它自己的尾部传递给一个函数 - 没有任何突变让你搞砸了,编译器通常非常聪明地优化这样的代码。
巧合的是,如果您喜欢列表推导并且不介意启用ParallelListComp
扩展程序,则可以像这样编写zipWith (-) (tail ls) ls
:
[b - a | a <- ls | b <- tail ls]
答案 1 :(得分:25)
在clojure中,您可以使用map
函数:
(defn diff [coll]
(map - coll (rest coll)))
答案 2 :(得分:13)
您还可以对连续元素进行模式匹配。在OCaml:
let rec diff = function
| [] | [_] -> []
| x::(y::_ as t) -> (y-x) :: diff t
通常的尾递归版本:
let diff =
let rec aux accu = function
| [] | [_] -> List.rev accu
| x::(y::_ as t) -> aux ((y-x)::accu) t in
aux []
答案 3 :(得分:7)
对于另一个Clojure解决方案,请尝试
(map (fn [[a b]] (- b a))
(partition 2 1 coll))
答案 4 :(得分:5)
只是为了补充惯用的答案:在函数式语言中,使用状态对象处理列表是可能的,就像您描述的那样。在存在更简单的解决方案的情况下,绝对不鼓励这种情况。
以下示例通过计算新的“state”并递归地将其传递给self来实现迭代。
(defn diffs
([coll] (diffs (rest coll) (first coll) []))
([coll prev acc]
(if-let [s (seq coll)]
; new 'state': rest of the list, head as the next 'prev' and
; diffs with the next difference appended at the end:
(recur (rest s) (first s) (conj acc (- (first s) prev)))
acc)))
状态在列表的前一个(prev
)值中表示,到目前为止计算的差异(acc
)和列表的其余部分留待处理(coll
)。
答案 5 :(得分:4)
这是如何在没有任何标准函数的情况下在Haskell中完成的,只是递归和模式匹配:
diff :: [Int] -> [Int]
diff [] = []
diff (x:xs) = hdiff xs x
hdiff :: [Int] -> Int -> [Int]
hdiff [] p = []
hdiff (x:xs) p = (x-p):hdiff xs x
答案 6 :(得分:1)
好的,这里有两个感兴趣的C#版本:
首先,学习错误的版本,或者之前命令式的版本(换句话说,我)可能会尝试编写函数式编程:
private static IEnumerable<int> ComputeUsingFold(IEnumerable<int> source)
{
var seed = new {Result = new List<int>(), Previous = 0};
return source.Aggregate(
seed,
(aggr, item) =>
{
if (aggr.Result.Count > 0)
{
aggr.Result.Add(item - aggr.Previous);
}
return new { Result = aggr.Result, Previous = item };
}).Result;
}
然后使用在这个问题的其他答案中表达的习语的更好的版本:
private static IEnumerable<int> ComputeUsingMap(IEnumerable<int> source)
{
return source.Zip(source.Skip(1), (f, s) => s - f);
}
我不确定,但在这个版本中,源枚举可能会被迭代两次。