You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无Clojure基础求助:基于Reducer实现map的代码逻辑解析

理解基于Reducible集合实现的my-map函数

我正在阅读《Seven Concurrency Models in Seven Weeks》,无Clojure前置知识,难以理解下方基于reducible集合实现map函数的代码。我在代码中标记了具体疑问,但更关注整体代码逻辑;已知coll-reduce为序列对象定义,但不清楚其在该代码中如何用于实现map,恳请提供提示。

;---
; Excerpted from "Seven Concurrency Models in Seven Weeks",
; published by The Pragmatic Bookshelf.
; Copyrights apply to this code. It may not be used to create training material,
; courses, books, articles, and the like. Contact us if you are in doubt.
; We make no guarantees that this code is fit for any purpose.
;---
(ns reducers.core
  (:require [clojure.core.protocols :refer [CollReduce coll-reduce]]
            [clojure.core.reducers :refer [CollFold coll-fold]]))

(defn make-reducer [reducible transformf]
  (reify
    CollReduce
    (coll-reduce [_ f1]
      (coll-reduce reducible (transformf f1) (f1))) ; what's the meaning of (f1) here?
    (coll-reduce [_ f1 init]
      (coll-reduce reducible (transformf f1) init))))

(defn my-map [mapf reducible]
  (make-reducer reducible
                (fn [reducef]
                  (fn [acc v]
                    (reducef acc (mapf v))))))

(into [] (my-map (partial * 2) [1 2 3 4])) ; I don't really understand what's reducef and what's acc here

整体逻辑拆解

这个实现的核心是通过包装原集合的reduce行为来实现map功能,和普通map先生成中间序列再遍历的思路完全不同。Reducible集合的本质是让集合自己掌控“如何被reduce”,我们这里就是给原集合的reduce过程“加一层拦截”:每个元素先经过映射函数处理,再交给原本的reduce逻辑。

1. my-map的核心作用

my-map接收两个参数:

  • mapf:你要对每个元素执行的映射逻辑(比如示例里的(partial *2))
  • reducible:要处理的原集合(比如示例里的[1 2 3 4])

它做的事情很简单:调用make-reducer,传入原集合和一个转换函数——这个转换函数的任务是把“原生的reduce函数”,包装成“先映射元素再执行reduce”的新函数。

2. make-reducer的角色

make-reducer返回一个实现了CollReduce协议的对象,这个对象的核心逻辑是:
当外部对它调用coll-reduce时,它会把工作委托给原集合的coll-reduce,但先用transformf把传入的reduce函数包装一层。

换句话说,你对my-map返回的对象做reduce,本质上是在对原集合做reduce,但每个元素都会先经过mapf处理。

3. 逐个解答你的疑问

  • 疑问1:(coll-reduce [_ f1]里的(f1)是什么意思?
    这是在模拟Clojure原生reduce的无初始值行为:当你调用(reduce f coll)时,Clojure会把集合的第一个元素作为初始值,后续用f处理剩余元素。而(f1)是Clojure的一个约定——调用reduce函数不带参数,就能获取它的“默认初始值”,比如(+)返回0,(*)返回1,(conj)返回空集合。这里就是用(f1)作为初始值,传给原集合的coll-reduce,和原生reduce逻辑对齐。

  • 疑问2:reducef和acc到底是什么?

    • reducef:是最终要执行的reduce函数。比如示例里的into [],内部会用conj作为reduce函数来收集元素,这个conj就是这里的reducef。
    • acc:是reduce过程中的累积值(accumulator)。比如用conj收集向量时,acc就是当前已经组装好的向量,每次调用(reducef acc (mapf v)),就是把映射后的元素(mapf v)加到acc里,得到新的累积值。

用示例走一遍完整流程

拿(into [] (my-map (partial *2) [1 2 3 4]))来说:

  1. into会调用coll-reduce处理my-map返回的对象,传入的f1是conj,初始值init是空向量[]。
  2. make-reducer返回的对象,会调用原集合[1 2 3 4]的coll-reduce,但把conj用my-map里的转换函数包装成:(fn [acc v] (conj acc (*2 v)))。
  3. 原集合的coll-reduce用这个包装后的函数,从[]开始依次处理元素:
    • 处理1:(conj [] 2) → [2]
    • 处理2:(conj [2] 4) → [2, 4]
    • 处理3:(conj [2, 4] 6) → [2, 4, 6]
    • 处理4:(conj [2, 4, 6] 8) → [2, 4, 6, 8]
  4. 最后返回这个累积结果,也就是into需要的向量。

这种方式避免了生成中间序列,直接在reduce过程中处理元素,这也是reducers比普通序列操作效率更高的原因。

内容的提问来源于stack exchange,提问作者yuez

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:07:30