【问题标题】:Clojure iteration over vector of functionsClojure 对函数向量的迭代
【发布时间】:2020-01-03 14:27:43
【问题描述】:

我正在读一本关于 Clojure 的书,上面写着:

“您可以使用 map 做的另一件有趣的事情是向它传递一组函数。如果您想对不同的数字集合执行一组计算,可以使用它,如下所示:”

(def sum #(reduce + %))
(def avg #(/ (sum %) (count %)))
(defn stats
  [numbers]
  (map #(% numbers) [sum count avg]))

(stats [3 4 10])
; => (17 3 17/3)

(stats [80 1 44 13 6])
; => (144 5 144/5)

“在本例中,stats 函数遍历函数向量,将每个函数应用于数字。”

我觉得这很令人困惑,书中没有给出更多解释。

我知道% 代表匿名函数中的参数,但我无法弄清楚它们在此示例中代表什么值。 % 是什么?

如果count 嵌套在avg 中,stats 如何迭代count

非常感谢。

【问题讨论】:

标签: clojure


【解决方案1】:

不要考虑“正在执行的代码”,而是考虑“减少表达式树”会有所帮助。表达式树被重写,直到结果出现。当“实时函数”出现在列表的第一个位置时,符号被“它们所代表的”替换,并且函数被应用于它们的参数;如(some-function a b c)。这是从表达式树的顶部到叶子以自上而下的方式完成的,当遇到quote 符号时停止。

在下面的示例中,很遗憾,由于不支持着色,我们无法标记已减少的内容和未减少的内容。请注意,减少的顺序不一定与 Clojure 编译器发出的编译代码实际执行的操作相对应。

开始于:

(defn stats
  [numbers]
  (map #(% numbers) [sum count avg]))

...我们会打电话给stats

第一个困难是stats 可以用集合作为一个单一的东西来调用:

(stats [a0 a1 a2 ... an])

或者它可以用一系列值来调用:

(stats a0 a1 a2 ... an)

它是什么?不幸的是,预期的调用风格只能通过查看函数定义来找到。在这种情况下,定义说

(defn stats [numbers] ...

这意味着stats 需要一个名为numbers 的东西。因此我们这样称呼它:

(stats [3 4 10])

现在开始减少!作为参数的数字向量被归约为自身,因为向量的每个元素都被归约并且数字归约为自身。符号stats 简化为之前声明的函数。 stats的定义其实是:

(fn [numbers] (map #(% numbers) [sum count avg]))

...defn 简写有点隐藏。因此

(stats [3 4 10])

变成

((fn [numbers] (map #(% numbers) [sum count avg])) [3 4 10])

接下来,减少fn 表达式会产生一个带有一个参数的实时函数。让我们用 ★ 标记实时函数,然后使用数学箭头符号:

(★(numbers ➜ (map #(% numbers) [sum count avg])) [3 4 10])

live 函数位于列表的第一个位置,因此随后会调用函数。函数调用包括将 numbers 的出现替换为实时函数主体中的参数 [3 4 10] 并剥离整个表达式的外括号:

(map #(% [3 4 10]) [sum count avg])

符号 mapsumcountavg 解析为已知的、已定义的函数,其中 mapcount 来自 Clojure 核心库,其余部分已在前面定义。同样,我们将它们标记为实时:

(★map #(% [3 4 10]) [★sum ★count ★avg]))

同样,# % 表示法是一个函数的简写,它接受一个参数并将其插入到 % 位置,让我们明确这一点:

(★map (fn [x] (x [3 4 10])) [★sum ★count ★avg]))

减少fn 表达式会产生一个参数的实时函数。同样,用 ★ 标记,让我们使用数学箭头符号:

(★map ★(x ➜ (x [3 4 10])) [★sum ★count ★avg]))

一个活动函数★map 处于头部位置,因此整个表达式根据map 的规范进行了缩减:将第一个参数(一个函数)应用于第二个参数(一个集合)的每个元素。我们可以假设先创建集合,然后进一步评估集合成员,所以:

[(★(x ➜ (x [3 4 10])) ★sum)
 (★(x ➜ (x [3 4 10])) ★count)
 (★(x ➜ (x [3 4 10])) ★avg)]

集合的每个元素都可以进一步减少,因为每个元素都有一个在头部位置有 1 个参数和一个可用参数的实时函数。因此,在每种情况下,x 都会被适当地替换:

[(★sum [3 4 10])
 (★count [3 4 10])
 (★avg [3 4 10])]

集合的每个元素都可以进一步减少,因为每个元素在头部位置都有一个 1 参数的实时函数。练习继续:

[ ((fn [x] (reduce + x)) [3 4 10])
  (★count [3 4 10])
  ((fn [x] (/ (sum x) (count x))) [3 4 10])]

然后

[ (★(x ➜ (reduce + x)) [3 4 10])
  3
  (★(x ➜ (/ (sum x) (count x))) [3 4 10])]

然后

[ (reduce + [3 4 10])
  3
  (/ ((fn [x] (reduce + x)) [3 4 10]) (count [3 4 10]))]

然后

[ (★reduce ★+ [3 4 10])
  3
  (/ (*(x ➜ (reduce + x)) [3 4 10]) (count [3 4 10]))]

然后

[ (★+ (★+ 3 4) 10)
  3
  (/ (reduce + [3 4 10]) (count [3 4 10]))]

然后

[ (★+ 7 10)
  3
  (★/ (★reduce ★+ [3 4 10]) (★count [3 4 10]))]

然后

[ 17
  3
  (★/ 17 3)]

终于

[ 17
  3
  17/3]

您也可以使用函数juxt。在 REPL 上尝试(doc juxt)

clojure.core/juxt
([f] [f g] [f g h] [f g h & fs])
  Takes a set of functions and returns a fn that is the juxtaposition
  of those fns.  The returned fn takes a variable number of args, and
  returns a vector containing the result of applying each fn to the
  args (left-to-right).
  ((juxt a b c) x) => [(a x) (b x) (c x)]

让我们试试吧!

(def sum #(reduce + %))
(def avg #(/ (sum %) (count %)))
((juxt sum count avg) [3 4 10])
;=> [17 3 17/3]
((juxt sum count avg) [80 1 44 13 6])
;=> [144 5 144/5]

因此我们可以将stats 定义为

(defn stats [numbers] ((juxt sum count avg) numbers))
(stats [3 4 10])
;=> [17 3 17/3]
(stats [80 1 44 13 6])
;=> [144 5 144/5]

附言

有时 Clojure 代码很难阅读,因为您不知道自己在处理什么“东西”。标量、集合或函数没有特殊的句法标记,实际上集合可以作为函数出现,或者标量可以是集合。与 Perl 比较,它有符号 $scalar@collection%hashmapfunction,还有 $reference-to-stuff$$scalarly-dereferenced-stuff@$collectionly-dereferenced-stuff%$hashmapply-dereferenced-stuff)。

【讨论】:

  • 感谢您抽出宝贵时间给出如此详尽的答复!我要完成它几次,因为那里有很多东西要吸收!
  • 如果你拿一张 A3 纸,边走边记下表达式树,其实很好。应该有软件来显示树并通过单击表达式来执行归约。
【解决方案2】:

% 代表匿名函数的第一个参数。

(map #(% numbers) [sum count avg]))

等价于:

(map (fn [f] (f numbers)) [sum count avg])

我对匿名函数使用了常规版本而不是简短版本,并将参数明确命名为“f”。有关简短版本的更完整说明,请参阅 https://practicalli.github.io/clojure/defining-behaviour-with-functions/anonymous-functions.html

在 Clojure 中,函数是一等公民,因此它们可以被视为值并传递给函数。当函数作为值传递时,这称为生成高阶函数(参见https://clojure.org/guides/higher_order_functions)。

【讨论】:

  • 谢谢,但为什么(map #(% numbers) [sum count avg])) 不只是(map numbers [sum count avg]))#(% numbers) 已经有 numbers 了,为什么还需要 %
  • @tommmmmmmy - numbers 是一个向量,目的是将每个函数依次应用于numbers(map numbers [sum count avg]) 将导致将向量 numbers 作为函数应用到参数 sumcountavg(向量是 clojure 中的函数,但需要单个索引参数,而不是函数)。
  • 嗯好的,谢谢;)。我想我得再读点书了;)
  • @tommmmmmmy 知道地图等效于以下 for 循环是否有帮助? (for [f [sum avg count]] (f numbers)),我们在这里迭代函数数组。每个时间函数 f 都应用于数字向量。
  • 谢谢,f 是什么?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-06
  • 2014-10-25
  • 2016-05-11
  • 1970-01-01
  • 2010-10-02
  • 2011-08-05
相关资源
最近更新 更多