├── .gitignore
├── .travis.yml
├── README.md
├── bb.edn
├── bin
└── kaocha
├── build.clj
├── deps.edn
├── dev
└── user.clj
├── package.json
├── src
└── net
│ └── cgrand
│ ├── xforms.cljc
│ └── xforms
│ ├── io.clj
│ ├── nodejs
│ └── stream.cljs
│ └── rfs.cljc
├── test
└── net
│ └── cgrand
│ └── xforms_test.cljc
└── tests.edn
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/clojure
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=clojure
3 |
4 | ### Clojure ###
5 | pom.xml
6 | pom.xml.asc
7 | *.jar
8 | *.class
9 | /lib/
10 | /classes/
11 | /bin/
12 | /node_modules/
13 | /.lumo_cache/
14 | /target/
15 | /checkouts/
16 | .lein-deps-sum
17 | .lein-repl-history
18 | .lein-plugins/
19 | .lein-failures
20 | .nrepl-port
21 | .cpcache/
22 | .clj-kondo/*/
23 | /cljs-test-runner-out/
24 |
25 | # End of https://www.toptal.com/developers/gitignore/api/clojure
26 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: clojure
2 | lein: lein
3 | script: lein test
4 | jdk:
5 | - openjdk6
6 | - openjdk7
7 | - oraclejdk7
8 | - oraclejdk8
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # xforms
2 |
3 | More transducers and reducing functions for Clojure(script)!
4 |
5 | *Transducers* can be classified in three groups: regular ones, higher-order ones
6 | (which accept other transducers as arguments) and aggregators (transducers which emit only 1 item out no matter how many went in).
7 | Aggregators generally only make sense in the context of a higher-order transducer.
8 |
9 | In `net.cgrand.xforms`:
10 |
11 | * regular ones: `partition` (1 arg), `reductions`, `for`, `take-last`, `drop-last`, `sort`, `sort-by`, `wrap`, `window` and `window-by-time`
12 | * higher-order ones: `by-key`, `into-by-key`, `multiplex`, `transjuxt`, `partition` (2+ args), `time`
13 | * aggregators: `reduce`, `into`, `without`, `transjuxt`, `last`, `count`, `avg`, `sd`, `min`, `minimum`, `max`, `maximum`, `str`
14 |
15 | In `net.cgrand.xforms.io`:
16 | * `sh` to use any process as a reducible collection (of stdout lines) or as a transducers (input as stdin lines, stdout lines as output).
17 |
18 |
19 | *Reducing functions*
20 |
21 | * in `net.cgrand.xforms.rfs`: `min`, `minimum`, `max`, `maximum`, `str`, `str!`, `avg`, `sd`, `last` and `some`.
22 | * in `net.cgrand.xforms.io`: `line-out` and `edn-out`.
23 |
24 | (in `net.cgrand.xforms`)
25 |
26 | *Transducing contexts*:
27 |
28 | * in `net.cgrand.xforms`: `transjuxt` (for performing several transductions in a single pass), `iterator` (clojure only), `into`, `without`, `count`, `str` (2 args) and `some`.
29 | * in `net.cgrand.xforms.io`: `line-out` (3+ args) and `edn-out` (3+ args).
30 | * in `net.cgrand.xforms.nodejs.stream`: `transformer`.
31 |
32 | *Reducible views* (in `net.cgrand.xforms.io`): `lines-in` and `edn-in`.
33 |
34 | **Note:** it should always be safe to update to the latest xforms version; short of bugfixes, breaking changes are avoided.
35 |
36 | ## Add as a dependency
37 |
38 | For specific coordinates see the [Releases](https://github.com/cgrand/xforms/releases) page.
39 |
40 | ## Usage
41 |
42 | ```clj
43 | => (require '[net.cgrand.xforms :as x])
44 | ```
45 |
46 | `str` and `str!` are two reducing functions to build Strings and StringBuilders in linear time.
47 |
48 | ```clj
49 | => (quick-bench (reduce str (range 256)))
50 | Execution time mean : 58,714946 µs
51 | => (quick-bench (reduce rf/str (range 256)))
52 | Execution time mean : 11,609631 µs
53 | ```
54 |
55 | `for` is the transducing cousin of `clojure.core/for`:
56 |
57 | ```clj
58 | => (quick-bench (reduce + (for [i (range 128) j (range i)] (* i j))))
59 | Execution time mean : 514,932029 µs
60 | => (quick-bench (transduce (x/for [i % j (range i)] (* i j)) + 0 (range 128)))
61 | Execution time mean : 373,814060 µs
62 | ```
63 |
64 | You can also use `for` like `clojure.core/for`: `(x/for [i (range 128) j (range i)] (* i j))` expands to `(eduction (x/for [i % j (range i)] (* i j)) (range 128))`.
65 |
66 | `by-key` and `reduce` are two new transducers. Here is an example usage:
67 |
68 | ```clj
69 | ;; reimplementing group-by
70 | (defn my-group-by [kfn coll]
71 | (into {} (x/by-key kfn (x/reduce conj)) coll))
72 |
73 | ;; let's go transient!
74 | (defn my-group-by [kfn coll]
75 | (into {} (x/by-key kfn (x/into [])) coll))
76 |
77 | => (quick-bench (group-by odd? (range 256)))
78 | Execution time mean : 29,356531 µs
79 | => (quick-bench (my-group-by odd? (range 256)))
80 | Execution time mean : 20,604297 µs
81 | ```
82 |
83 | Like `by-key`, `partition` also takes a transducer as last argument to allow further computation on the partition.
84 |
85 | ```clj
86 | => (sequence (x/partition 4 (x/reduce +)) (range 16))
87 | (6 22 38 54)
88 | ```
89 |
90 | Padding is achieved as usual:
91 |
92 | ```clj
93 | => (sequence (x/partition 4 4 (repeat :pad) (x/into [])) (range 9))
94 | ([0 1 2 3] [4 5 6 7] [8 :pad :pad :pad])
95 | ```
96 |
97 |
98 | `avg` is a transducer to compute the arithmetic mean. `transjuxt` is used to perform several transductions at once.
99 |
100 | ```clj
101 | => (into {} (x/by-key odd? (x/transjuxt [(x/reduce +) x/avg])) (range 256))
102 | {false [16256 127], true [16384 128]}
103 | => (into {} (x/by-key odd? (x/transjuxt {:sum (x/reduce +) :mean x/avg :count x/count})) (range 256))
104 | {false {:sum 16256, :mean 127, :count 128}, true {:sum 16384, :mean 128, :count 128}}
105 | ```
106 |
107 | `window` is a new transducer to efficiently compute a windowed accumulator:
108 |
109 | ```clj
110 | ;; sum of last 3 items
111 | => (sequence (x/window 3 + -) (range 16))
112 | (0 1 3 6 9 12 15 18 21 24 27 30 33 36 39 42)
113 |
114 | => (def nums (repeatedly 8 #(rand-int 42)))
115 | #'user/nums
116 | => nums
117 | (11 8 32 26 6 10 37 24)
118 |
119 | ;; avg of last 4 items
120 | => (sequence
121 | (x/window 4 rf/avg #(rf/avg %1 %2 -1))
122 | nums)
123 | (11 19/2 17 77/4 18 37/2 79/4 77/4)
124 |
125 | ;; min of last 3 items
126 | => (sequence
127 | (x/window 3
128 | (fn
129 | ([] (sorted-map))
130 | ([m] (key (first m)))
131 | ([m x] (update m x (fnil inc 0))))
132 | (fn [m x]
133 | (let [n (dec (m x))]
134 | (if (zero? n)
135 | (dissoc m x)
136 | (assoc m x (dec n))))))
137 | nums)
138 | (11 8 8 8 6 6 6 10)
139 | ```
140 |
141 | ## On Partitioning
142 |
143 | Both `by-key` and `partition` takes a transducer as parameter. This transducer is used to further process each partition.
144 |
145 | It's worth noting that all transformed outputs are subsequently interleaved. See:
146 |
147 | ```clj
148 | => (sequence (x/partition 2 1 identity) (range 8))
149 | (0 1 1 2 2 3 3 4 4 5 5 6 6 7)
150 | => (sequence (x/by-key odd? identity) (range 8))
151 | ([false 0] [true 1] [false 2] [true 3] [false 4] [true 5] [false 6] [true 7])
152 | ```
153 |
154 | That's why most of the time the last stage of the sub-transducer will be an aggregator like `x/reduce` or `x/into`:
155 |
156 | ```clj
157 | => (sequence (x/partition 2 1 (x/into [])) (range 8))
158 | ([0 1] [1 2] [2 3] [3 4] [4 5] [5 6] [6 7])
159 | => (sequence (x/by-key odd? (x/into [])) (range 8))
160 | ([false [0 2 4 6]] [true [1 3 5 7]])
161 | ```
162 |
163 | ## Simple examples
164 |
165 | `(group-by kf coll)` is `(into {} (x/by-key kf (x/into []) coll))`.
166 |
167 | `(plumbing/map-vals f m)` is `(into {} (x/by-key (map f)) m)`.
168 |
169 | My faithful `(reduce-by kf f init coll)` is now `(into {} (x/by-key kf (x/reduce f init)))`.
170 |
171 | `(frequencies coll)` is `(into {} (x/by-key identity x/count) coll)`.
172 |
173 | ## On key-value pairs
174 |
175 | Clojure `reduce-kv` is able to reduce key value pairs without allocating vectors or map entries: the key and value
176 | are passed as second and third arguments of the reducing function.
177 |
178 | Xforms allows a reducing function to advertise its support for key value pairs (3-arg arity) by implementing the `KvRfable` protocol (in practice using the `kvrf` macro).
179 |
180 | Several xforms transducers and transducing contexts leverage `reduce-kv` and `kvrf`. When these functions are used together, pairs can be transformed without being allocated.
181 |
182 |
183 |
184 | fn | kvs in? | kvs out?
185 | |
186 |
187 | `for` | when first binding is a pair | when `body-expr` is a pair
188 | |
`reduce` | when is `f` is a kvrf | no
189 | |
1-arg `into` (transducer) | when `to` is a map | no
190 | |
3-arg `into` (transducing context) | when `from` is a map | when `to` is a map
191 | |
`by-key` (as a transducer) | when is `kfn` and `vfn` are unspecified or `nil` | when `pair` is `vector` or unspecified
192 | |
`by-key` (as a transducing context on values) | no | no
193 | |
194 |
195 |
196 | ```clj
197 | ;; plain old sequences
198 | => (let [m (zipmap (range 1e5) (range 1e5))]
199 | (crit/quick-bench
200 | (into {}
201 | (for [[k v] m]
202 | [k (inc v)]))))
203 | Evaluation count : 12 in 6 samples of 2 calls.
204 | Execution time mean : 55,150081 ms
205 | Execution time std-deviation : 1,397185 ms
206 |
207 | ;; x/for but pairs are allocated (because of into)
208 | => (let [m (zipmap (range 1e5) (range 1e5))]
209 | (crit/quick-bench
210 | (into {}
211 | (x/for [[k v] _]
212 | [k (inc v)])
213 | m)))
214 | Evaluation count : 18 in 6 samples of 3 calls.
215 | Execution time mean : 39,119387 ms
216 | Execution time std-deviation : 1,456902 ms
217 |
218 | ;; x/for but no pairs are allocated (thanks to x/into)
219 | => (let [m (zipmap (range 1e5) (range 1e5))]
220 | (crit/quick-bench (x/into {}
221 | (x/for [[k v] %]
222 | [k (inc v)])
223 | m)))
224 | Evaluation count : 24 in 6 samples of 4 calls.
225 | Execution time mean : 24,276790 ms
226 | Execution time std-deviation : 364,932996 µs
227 | ```
228 |
229 | ## Changelog
230 |
231 | ### 0.19.6
232 |
233 | * Fix regression in 0.19.5 #54
234 |
235 | ### 0.19.5
236 |
237 | * Support ClojureDart
238 |
239 | ### 0.19.4
240 |
241 | * Fix ClojureScript compilation broken in `0.19.3` #49
242 | * Fix `x/sort` and `x/sort-by` for ClojureScript #40
243 |
244 | ### 0.19.3
245 |
246 | * Add `deps.edn` to enable usage as a [git library](https://clojure.org/guides/deps_and_cli#_using_git_libraries)
247 | * Bump `macrovich` to make Clojure and ClojureScript provided dependencies #34
248 | * Fix reflection warnings in `xforms.io` #35 #36
249 | * Add compatibility with [babashka](https://github.com/babashka/babashka) #42
250 | * Fix `x/destructuring-pair?` #44 #45
251 | * Fix `x/into` performance hit with small maps #46 #47
252 | * Fix reflection and shadowing warnings in tests
253 |
254 | ### 0.19.2
255 |
256 | * Fix infinity symbol causing issues with ClojureScript #31
257 |
258 | ### 0.19.0
259 |
260 | `time` allows to measure time spent in one transducer (excluding time spent downstream).
261 |
262 | ```clj
263 | => (time ; good old Clojure time
264 | (count (into [] (comp
265 | (x/time "mapinc" (map inc))
266 | (x/time "filterodd" (filter odd?))) (range 1e6))))
267 | filterodd: 61.771738 msecs
268 | mapinc: 143.895317 msecs
269 | "Elapsed time: 438.34291 msecs"
270 | 500000
271 | ```
272 |
273 | First argument can be a function that gets passed the time (in ms),
274 | this allows for example to log time instead of printing it.
275 |
276 | ### 0.9.5
277 |
278 | * Short (up to 4) literal collections (or literal collections with `:unroll` metadata) in collection positions in `x/for` are unrolled.
279 | This means that the collection is not allocated.
280 | If it's a collection of pairs (e.g. maps), pairs themselves won't be allocated.
281 |
282 | ### 0.9.4
283 |
284 | * Add `x/into-by-key` short hand
285 |
286 | ### 0.7.2
287 |
288 | * Fix transients perf issue in Clojurescript
289 |
290 | ### 0.7.1
291 |
292 | * Works with Clojurescript (even self-hosted).
293 |
294 | ### 0.7.0
295 |
296 | * Added 2-arg arity to `x/count` where it acts as a transducing context e.g. `(x/count (filter odd?) (range 10))`
297 | * Preserve type hints in `x/for` (and generally with `kvrf`).
298 |
299 | ### 0.6.0
300 |
301 | * Added `x/reductions`
302 | * Now if the first collection expression in `x/for` is not a placeholder then `x/for` works like `x/for` but returns an eduction and performs all iterations using reduce.
303 |
304 | ## Troubleshooting xforms in a Clojurescript dev environment
305 |
306 | If you use xforms with Clojurescript and the Emacs editor to start your figwheel REPL be sure to include the `cider.nrepl/cider-middleware` to your figwheel's nrepl-middleware.
307 | ```
308 | :figwheel {...
309 | :nrepl-middleware [cider.nrepl/cider-middleware;;<= that middleware
310 | refactor-nrepl.middleware/wrap-refactor
311 | cemerick.piggieback/wrap-cljs-repl]
312 | ...}
313 | ```
314 | Otherwise a strange interaction occurs and every results from your REPL evaluation would be returned as a String. Eg.:
315 | ```
316 | cljs.user> 1
317 | "1"
318 | cljs.user>
319 | ```
320 | instead of:
321 | ```
322 | cljs.user> 1
323 | 1
324 | cljs.user>
325 | ```
326 |
327 |
328 | ## License
329 |
330 | Copyright © 2015-2016 Christophe Grand
331 |
332 | Distributed under the Eclipse Public License either version 1.0 or (at
333 | your option) any later version.
334 |
--------------------------------------------------------------------------------
/bb.edn:
--------------------------------------------------------------------------------
1 | {:deps {local/deps {:local/root "."}}
2 | :paths ["src" "test"]
3 |
4 | :tasks
5 | {:requires ([clojure.string :as str])
6 |
7 | :init
8 | (do
9 | (defn kaocha [alias args]
10 | (apply shell "bin/kaocha" alias args))
11 |
12 | (defn test-cljs [alias args]
13 | (apply clojure (str/join ["-M:test:cljs-test-runner" alias]) args)))
14 |
15 | test-clj-9
16 | {:task (kaocha :clj-1-9 *command-line-args*)}
17 |
18 | test-clj-10
19 | {:task (kaocha :clj-1-10 *command-line-args*)}
20 |
21 | test-clj-11
22 | {:task (kaocha :clj-1-11 *command-line-args*)}
23 |
24 | test-clj
25 | {:depends [test-clj-9 test-clj-10 test-clj-11]}
26 |
27 | test-cljs-9
28 | {:task (test-cljs :clj-1-9 *command-line-args*)}
29 |
30 | test-cljs-10
31 | {:task (test-cljs :clj-1-10 *command-line-args*)}
32 |
33 | test-cljs-11
34 | {:task (test-cljs :clj-1-11 *command-line-args*)}
35 |
36 | test-cljs
37 | {:depends [#_test-cljs-9 test-cljs-10 test-cljs-11]}
38 |
39 | test-bb
40 | {:requires ([clojure.test :as t]
41 | [net.cgrand.xforms-test])
42 | :task (t/run-tests 'net.cgrand.xforms-test)}
43 |
44 | test-all
45 | {:depends [test-bb test-clj test-cljs]}
46 |
47 | perf-bb
48 | {:requires ([net.cgrand.xforms :as x])
49 | :task
50 | (let [n 10000
51 | m (zipmap (range 100) (range))
52 | mapping (map (fn [[k v]] [k (inc v)]))
53 | xforing (x/for [[k v] _] [k (inc v)])]
54 | (time (dotimes [_ n] (into {} mapping m)))
55 | (time (dotimes [_ n] (into {} xforing m)))
56 | (time (dotimes [_ n] (x/into {} xforing m))))}}}
57 |
--------------------------------------------------------------------------------
/bin/kaocha:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -x
3 | clojure -Srepro -M:kaocha:test"$1" "${@:2}"
4 |
--------------------------------------------------------------------------------
/build.clj:
--------------------------------------------------------------------------------
1 | (ns build
2 | (:require [clojure.tools.build.api :as b]
3 | [clojure.java.shell :as sh]))
4 |
5 | (def lib 'net.cgrand/xforms)
6 | (def version "0.19.6" #_(format "0.0.%s" (b/git-count-revs nil)))
7 | (def class-dir "target/classes")
8 | (def basis (b/create-basis {:project "deps.edn"}))
9 | (def jar-file (format "target/%s-%s.jar" (name lib) version))
10 | (def scm {:connection "scm:git:git://github.com/cgrand/xforms.git"
11 | :developerConnection "scm:git:git://github.com/cgrand/xforms.git"
12 | :url "https://github.com/cgrand/xforms"})
13 | (def extra-pom-data
14 | [[:licenses
15 | [:license
16 | [:name "Eclipse Public License 1.0"]
17 | [:url "https://opensource.org/license/epl-1-0/"]
18 | [:distribution "repo"]]
19 | [:license
20 | [:name "Eclipse Public License 2.0"]
21 | [:url "https://opensource.org/license/epl-2-0/"]
22 | [:distribution "repo"]]]])
23 |
24 | (defn clean [_]
25 | (b/delete {:path "target"}))
26 |
27 | (defn jar [_]
28 | (b/write-pom {:class-dir class-dir
29 | :lib lib
30 | :version version
31 | :basis basis
32 | :src-dirs ["src"]
33 | :scm (assoc scm :tag (str "v" version))
34 | :pom-data extra-pom-data})
35 | (b/copy-dir {:src-dirs ["src" "resources"]
36 | :target-dir class-dir})
37 | (b/jar {:class-dir class-dir
38 | :jar-file jar-file}))
39 |
40 | (defn clojars [_]
41 | (sh/sh
42 | "mvn" "deploy:deploy-file" (str "-Dfile=" jar-file)
43 | ;target/classes/META-INF/maven/net.cgrand/xforms/pom.xml
44 | (format "-DpomFile=%s/META-INF/maven/%s/%s/pom.xml"
45 | class-dir (namespace lib) (name lib))
46 | "-DrepositoryId=clojars" "-Durl=https://clojars.org/repo/"))
47 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:deps {net.cgrand/macrovich {:mvn/version "0.2.2"}}
2 | :paths ["src"]
3 |
4 | :aliases
5 | {:dev
6 | {:extra-paths ["dev"]}
7 |
8 | :cljd
9 | {:extra-deps
10 | {tensegritics/clojuredart
11 | {:git/url "https://github.com/tensegritics/ClojureDart.git"
12 | :sha "ae1b485e84ccc35b122f776dfc7cc62198274701"}}}
13 |
14 | :clj-1-9
15 | {:extra-deps
16 | {org.clojure/clojure {:mvn/version "1.9.0"}
17 | org.clojure/clojurescript {:mvn/version "1.9.293"}}}
18 |
19 | :clj-1-10
20 | {:extra-deps
21 | {org.clojure/clojure {:mvn/version "1.10.3"}
22 | org.clojure/clojurescript {:mvn/version "1.10.914"}}}
23 |
24 | :clj-1-11
25 | {:extra-deps
26 | {org.clojure/clojure {:mvn/version "1.11.1"}
27 | org.clojure/clojurescript {:mvn/version "1.11.60"}}}
28 |
29 | :test
30 | {:extra-paths ["test"]}
31 |
32 | :kaocha
33 | {:extra-paths ["test"]
34 | :extra-deps {lambdaisland/kaocha {:mvn/version "1.69.1069"}}
35 | :main-opts ["-m" "kaocha.runner"]}
36 |
37 | :cljs-test-runner
38 | {:extra-paths ["test"]
39 | :extra-deps {olical/cljs-test-runner {:mvn/version "3.8.0"}}
40 | :main-opts ["-m" "cljs-test-runner.main"]}
41 |
42 | :build
43 | {:paths ["."]
44 | :deps {io.github.clojure/tools.build {:git/tag "v0.9.6" :git/sha "8e78bcc"}}
45 | :ns-default build}}}
46 |
--------------------------------------------------------------------------------
/dev/user.clj:
--------------------------------------------------------------------------------
1 | (ns user)
2 |
3 | (set! *warn-on-reflection* true)
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xforms",
3 | "version": "0.16.0",
4 | "description": "Extra transducers for Clojurescript",
5 | "repository": "https://github.com/cgrand/xforms.git",
6 | "author": "Christophe Grand ",
7 | "license": "EPL-1.0",
8 | "directories": {
9 | "lib": "src",
10 | "cache": "./lumo-cache"
11 | },
12 | "keywords": [
13 | "cljs",
14 | "cljc",
15 | "self-host",
16 | "transducer"
17 | ],
18 | "dependencies": {
19 | "macrovich": "^0.2.1-SNAPSHOT"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/net/cgrand/xforms.cljc:
--------------------------------------------------------------------------------
1 | (ns net.cgrand.xforms
2 | "Extra transducers for Clojure"
3 | {:author "Christophe Grand"}
4 | #?(:cljs (:require-macros
5 | [net.cgrand.macrovich :as macros]
6 | [net.cgrand.xforms :refer [for kvrf let-complete]])
7 | :default (:require [net.cgrand.macrovich :as macros]))
8 | (:refer-clojure :exclude [some reduce reductions into count for partition
9 | str last keys vals min max drop-last take-last
10 | sort sort-by time #?@(:bb [] :cljd/clj-host [] :clj [satisfies?])])
11 | (:require [#?(:cljd cljd.core :clj clojure.core :cljs cljs.core) :as core]
12 | [net.cgrand.xforms.rfs :as rf]
13 | #?@(:cljd [["dart:collection" :as dart:coll]] :clj [[clojure.core.protocols]] :cljs []))
14 | #?(:cljd/clj-host
15 | ; customize the clj/jvm ns used for macroexpansion
16 | (:host-ns (:require [clojure.core :as core]
17 | [net.cgrand.macrovich :as macros])))
18 | #?(:cljs (:import [goog.structs Queue])))
19 |
20 | (defn- ^:macro-support pair? [x] (and (vector? x) (= 2 (core/count x))))
21 | (def ^:macro-support destructuring-pair?
22 | (let [kw-or-& #(or (keyword? %) (= '& %))]
23 | (fn [x]
24 | (and (pair? x)
25 | (not (kw-or-& (first x)))))))
26 |
27 |
28 |
29 | (macros/deftime
30 |
31 | (defn- ^:macro-support no-user-meta? [x]
32 | (= {} (dissoc (or (meta x) {}) :file :line :column :end-line :end-column)))
33 |
34 | (defmacro unreduced->
35 | "Thread first while threaded value is not reduced.
36 | Doesn't unreduce the final value."
37 | ([x] x)
38 | ([x expr & exprs]
39 | `(let [x# ~x]
40 | (if (reduced? x#)
41 | x#
42 | (unreduced-> (-> x# ~expr) ~@exprs)))))
43 |
44 | (defmacro for
45 | "Like clojure.core/for with the first expression being replaced by % (or _). Returns a transducer.
46 | When the first expression is not % (or _) returns an eduction."
47 | [[binding %or_ & seq-exprs] body-expr]
48 | (if-not (and (symbol? %or_) (#{"%" "_"} (name %or_)))
49 | `(eduction (for [~binding ~'% ~@seq-exprs] ~body-expr) ~%or_)
50 | (let [rf (gensym 'rf)
51 | acc (gensym 'acc)
52 | rpairs (core/partition 2 (rseq (vec seq-exprs)))
53 | build (fn [init]
54 | (core/reduce (fn [body [expr binding]]
55 | (case binding
56 | :let `(let ~expr ~body)
57 | :when `(if ~expr ~body ~acc)
58 | :while `(if ~expr ~body (reduced ~acc))
59 | (if (and (coll? expr) (not (seq? expr))
60 | (or (<= (core/count expr) 4) (:unroll (meta expr))))
61 | (let [body-rf (gensym 'body-rf)]
62 | (if (and (destructuring-pair? binding) (every? vector? expr))
63 | `(let [~body-rf (fn [~acc ~@binding] ~body)]
64 | (unreduced (unreduced-> ~acc
65 | ~@(map (fn [[k v]] `(~body-rf ~k ~v)) expr))))
66 | `(let [~body-rf (fn [~acc ~binding] ~body)]
67 | (unreduced (unreduced-> ~acc
68 | ~@(map (fn [v] `(~body-rf ~v)) expr))))))
69 | (if (destructuring-pair? binding)
70 | `(let [expr# ~expr]
71 | (if (and (map? expr#) (kvreducible? expr#))
72 | (core/reduce-kv (fn [~acc ~@binding] ~body) ~acc expr#)
73 | (core/reduce (fn [~acc ~binding] ~body) ~acc expr#)))
74 | `(core/reduce (fn [~acc ~binding] ~body) ~acc ~expr)))))
75 | init rpairs))
76 | nested-reduceds (core/for [[expr binding] rpairs
77 | :when (not (keyword? binding))]
78 | `reduced)
79 | body (build `(let [acc# (~rf ~acc ~@(if (and (pair? body-expr) (no-user-meta? body-expr))
80 | body-expr
81 | [body-expr]))]
82 | (if (reduced? acc#)
83 | (-> acc# ~@nested-reduceds)
84 | acc#)))]
85 | `(fn [~rf]
86 | (let [~rf (ensure-kvrf ~rf)]
87 | (kvrf
88 | ([] (~rf))
89 | ([~acc] (~rf ~acc))
90 | ([~acc ~binding] ~body)))))))
91 |
92 | (defn- ^:macro-support arity [[arglist & body :as fn-body]]
93 | (let [[fixargs varargs] (split-with (complement #{'&}) arglist)]
94 | (if (seq varargs) (zipmap (range (core/count fixargs) 4) (repeat fn-body)))
95 | {(core/count fixargs) fn-body}))
96 |
97 | (defmacro kvrf [name? & fn-bodies]
98 | (let [name (if (symbol? name?) name? (gensym '_))
99 | fn-bodies (if (symbol? name?) fn-bodies (cons name? fn-bodies))
100 | fn-bodies (if (vector? (first fn-bodies)) (list fn-bodies) fn-bodies)
101 | arities (core/into {} (mapcat arity) fn-bodies)
102 | _ (when-not (core/some arities [2 3]) (throw (ex-info "Either arity 2 or 3 should be defined in kvrf." {:form &form})))
103 | fn-bodies (cond-> fn-bodies
104 | (not (arities 3)) (conj (let [[[acc arg] & body] (arities 2)]
105 | (if (destructuring-pair? arg)
106 | (let [[karg varg] arg]
107 | `([~acc ~karg ~varg] ~@body))
108 | (let [k (gensym "k__")
109 | v (gensym "v__")
110 | arg-value (macros/case
111 | :clj `(clojure.lang.MapEntry. ~k ~v)
112 | :cljs [k v]
113 | :cljd `(MapEntry ~k ~v))]
114 | `([~acc ~k ~v] (let [~arg ~arg-value] ~@body))))))
115 | (not (arities 2)) (conj (let [[[acc karg varg] & body] (arities 3)]
116 | `([~acc [~karg ~varg]] ~@body))))]
117 | `(reify
118 | #?@(:bb [] ;; babashka currently only supports reify with one Java interface at a time
119 | :default [~@(macros/case :cljd '[cljd.core/Fn] :clj '[clojure.lang.Fn])])
120 | KvRfable
121 | (~'some-kvrf [this#] this#)
122 | ~(macros/case :cljs `core/IFn :clj 'clojure.lang.IFn :cljd 'cljd.core/IFn)
123 | ~@(core/for [[args & body] fn-bodies]
124 | (let [nohint-args (map (fn [arg] (if (:tag (meta arg)) (gensym 'arg) arg)) args)
125 | rebind (mapcat (fn [arg nohint]
126 | (when-not (= arg nohint) [arg nohint])) args nohint-args)]
127 | `(~(macros/case :cljd '-invoke :cljs `core/-invoke :clj 'invoke)
128 | [~name ~@nohint-args] ~@(if (seq rebind) [`(let [~@rebind] ~@body)] body)))))))
129 |
130 | (defmacro ^:private let-complete [[binding volatile] & body]
131 | `(let [v# @~volatile]
132 | (when-not (identical? v# ~volatile) ; self reference as sentinel
133 | (vreset! ~volatile ~volatile)
134 | (let [~binding v#]
135 | ~@body))))
136 | )
137 |
138 | (declare into reduce multiplex by-key)
139 |
140 | (defprotocol KvRfable "Protocol for reducing fns that accept key and val as separate arguments."
141 | (some-kvrf [f] "Returns a kvrf or nil"))
142 |
143 | (macros/usetime
144 |
145 | ;; Workaround clojure.core/satisfies? being slow in Clojure
146 | ;; see https://ask.clojure.org/index.php/3304/make-satisfies-as-fast-as-a-protocol-method-call
147 | #?(:bb nil
148 | :cljd nil
149 | :clj
150 | (defn fast-satisfies?-fn
151 | "Ported from https://github.com/clj-commons/manifold/blob/37658e91f836047a630586a909a2e22debfbbfc6/src/manifold/utils.clj#L77-L89"
152 | [protocol-var]
153 | (let [^java.util.concurrent.ConcurrentHashMap classes
154 | (java.util.concurrent.ConcurrentHashMap.)]
155 | (add-watch protocol-var ::memoization (fn [& _] (.clear classes)))
156 | (fn [x]
157 | (let [cls (class x)
158 | val (.get classes cls)]
159 | (if (nil? val)
160 | (let [val (core/satisfies? @protocol-var x)]
161 | (.put classes cls val)
162 | val)
163 | val))))))
164 |
165 | #?(:cljs
166 | (defn kvreducible? [coll]
167 | (satisfies? IKVReduce coll))
168 |
169 | :cljd
170 | (defn kvreducible? [coll]
171 | (satisfies? cljd.core/IKVReduce coll))
172 |
173 | :clj
174 | (let [satisfies-ikvreduce? #?(:bb #(satisfies? clojure.core.protocols/IKVReduce %)
175 | :default (fast-satisfies?-fn #'clojure.core.protocols/IKVReduce))]
176 | (if (satisfies-ikvreduce? (Object.))
177 | (defn kvreducible?
178 | "Clojure 1.11 makes everything satisfy IKVReduce, so we can short-circuit"
179 | [_] true)
180 | (defn kvreducible? [coll] (satisfies-ikvreduce? coll)))))
181 |
182 |
183 | (extend-protocol KvRfable
184 | #?(:cljd fallback :clj Object :cljs default) (some-kvrf [_] nil)
185 | #?@(:clj [nil (some-kvrf [_] nil)]))
186 |
187 | (defn ensure-kvrf [rf]
188 | (or (some-kvrf rf)
189 | (kvrf
190 | ([] (rf))
191 | ([acc] (rf acc))
192 | ([acc x] (rf acc x)))))
193 |
194 | (defn reduce
195 | "A transducer that reduces a collection to a 1-item collection consisting of only the reduced result.
196 | Unlike reduce but like transduce it does call the completing arity (1) of the reducing fn."
197 | ([f]
198 | (fn [rf]
199 | (let [vacc (volatile! (f))]
200 | (let [f (ensure-kvrf f)]
201 | (kvrf
202 | ([] (rf))
203 | ([acc] (let-complete [f-acc vacc]
204 | (rf (unreduced (rf acc (f (unreduced f-acc)))))))
205 | ([acc x]
206 | (if (reduced? (vswap! vacc f x))
207 | (reduced acc)
208 | acc))
209 | ([acc k v]
210 | (if (reduced? (vswap! vacc f k v))
211 | (reduced acc)
212 | acc)))))))
213 | ([f init]
214 | (reduce (fn ([] init) ([acc] (f acc)) ([acc x] (f acc x))))))
215 |
216 | (defn- into-rf [to]
217 | (cond
218 | #?(:cljd (satisfies? cljd.core/IEditableCollection to)
219 | :clj (instance? clojure.lang.IEditableCollection to)
220 | :cljs (satisfies? IEditableCollection to))
221 | (if (map? to)
222 | (kvrf
223 | ([] (transient to))
224 | ([acc] (persistent! acc))
225 | ([acc x] (conj! acc x))
226 | ([acc k v] (assoc! acc k v)))
227 | (fn
228 | ([] (transient to))
229 | ([acc] (persistent! acc))
230 | ([acc x] (conj! acc x))))
231 | (map? to)
232 | (kvrf
233 | ([] to)
234 | ([acc] acc)
235 | ([acc x] (conj acc x))
236 | ([acc k v] (assoc acc k v)))
237 | :else
238 | (fn
239 | ([] to)
240 | ([acc] acc)
241 | ([acc x] (conj acc x)))))
242 |
243 | (defn into
244 | "Like clojure.core/into but with a 1-arg arity returning a transducer which accumulate every input in a collection and outputs only the accumulated collection."
245 | ([to]
246 | (reduce (into-rf to)))
247 | ([to from]
248 | (into to identity from))
249 | ([to xform from]
250 | (let [rf (xform (into-rf to))]
251 | (if-let [rf (and (map? from) (kvreducible? from) (some-kvrf rf))]
252 | (rf (core/reduce-kv rf (rf) from))
253 | (rf (core/reduce rf (rf) from))))))
254 |
255 | (defn- without-rf [from]
256 | (cond
257 | #?(:cljd (satisfies? cljd.core/IEditableCollection from)
258 | :clj (instance? clojure.lang.IEditableCollection from)
259 | :cljs (satisfies? IEditableCollection from))
260 | (if (map? from)
261 | (fn
262 | ([] (transient from))
263 | ([acc] (persistent! acc))
264 | ([acc x] (dissoc! acc x)))
265 | (fn
266 | ([] (transient from))
267 | ([acc] (persistent! acc))
268 | ([acc x] (disj! acc x))))
269 | (map? from)
270 | (fn
271 | ([] from)
272 | ([acc] acc)
273 | ([acc x] (dissoc acc x)))
274 | :else
275 | (fn
276 | ([] from)
277 | ([acc] acc)
278 | ([acc x] (disj acc x)))))
279 |
280 | (defn without
281 | "The opposite of x/into: dissociate or disjoin from the target."
282 | ([target]
283 | (reduce (without-rf target)))
284 | ([target keys]
285 | (without target identity keys))
286 | ([target xform keys]
287 | (let [rf (xform (without-rf target))]
288 | (if-let [rf (and (map? keys) (kvreducible? keys) (some-kvrf rf))]
289 | (rf (core/reduce-kv rf (rf) keys))
290 | (rf (core/reduce rf (rf) keys))))))
291 |
292 | (defn minimum
293 | ([comparator]
294 | (minimum comparator nil))
295 | ([comparator absolute-maximum]
296 | (reduce (rf/minimum comparator absolute-maximum))))
297 |
298 | (defn maximum
299 | ([comparator]
300 | (maximum comparator nil))
301 | ([comparator absolute-minimum]
302 | (reduce (rf/maximum comparator absolute-minimum))))
303 |
304 | (def min (reduce rf/min))
305 |
306 | (def max (reduce rf/max))
307 |
308 | (defn str
309 | "When used as a value, it's an aggregating transducer that concatenates input values
310 | into a single output value.
311 | When used as a function of two args (xform and coll) it's a transducing context that
312 | concatenates all values in a string."
313 | {:arglists '([xform coll])}
314 | ([rf] ((reduce rf/str) rf))
315 | ([xform coll]
316 | (transduce xform rf/str coll)))
317 |
318 | (defn wrap
319 | "Transducer. Adds open as the first item, and close as the last. Optionally inserts delim between each input item."
320 | ([open close]
321 | (fn [rf]
322 | (let [vrf (volatile! nil)]
323 | (vreset! vrf
324 | (fn [acc x]
325 | (let [acc (rf acc open)]
326 | (vreset! vrf rf)
327 | (if (reduced? acc)
328 | acc
329 | (rf acc x)))))
330 | (fn
331 | ([] (rf))
332 | ([acc] (rf (unreduced (rf acc close))))
333 | ([acc x] (@vrf acc x))))))
334 | ([open close delim]
335 | (comp (interpose delim) (wrap open close))))
336 |
337 | (defn vals [rf]
338 | (kvrf
339 | ([] (rf))
340 | ([acc] (rf acc))
341 | ([acc k v] (rf acc v))))
342 |
343 | (defn keys [rf]
344 | (kvrf
345 | ([] (rf))
346 | ([acc] (rf acc))
347 | ([acc k v] (rf acc k))))
348 |
349 | ;; for both map entries and vectors
350 | (defn- key' [kv] (nth kv 0))
351 | (defn- val' [kv] (nth kv 1))
352 |
353 | (defn- nop-rf "The noop reducing function" ([acc] acc) ([acc _] acc) ([acc _ _] acc))
354 |
355 | (defn- multiplexable
356 | "Returns a multiplexable reducing function (doesn't init or complete the uderlying rf, wraps reduced -- like preserving-reduced)"
357 | [rf]
358 | (let [rf (ensure-kvrf rf)]
359 | (kvrf
360 | ([])
361 | ([acc] acc) ; no init no complete rf
362 | ([acc x]
363 | (let [acc (rf acc x)]
364 | (if (reduced? acc)
365 | (reduced acc)
366 | acc)))
367 | ([acc k v]
368 | (let [acc (rf acc k v)]
369 | (if (reduced? acc)
370 | (reduced acc)
371 | acc))))))
372 |
373 | (defn by-key
374 | "Returns a transducer which partitions items according to kfn.
375 | It applies the transform specified by xform to each partition.
376 | Partitions contain the \"value part\" (as returned by vfn) of each item.
377 | The resulting transformed items are wrapped back into a \"pair\" using the pair function.
378 | Default values for kfn, vfn and pair are first, second (or identity if kfn is specified) and vector."
379 | ([xform] (by-key nil nil vector xform))
380 | ([kfn xform] (by-key kfn identity vector xform))
381 | ([kfn vfn xform] (by-key kfn vfn vector xform))
382 | ([kfn vfn pair xform]
383 | (let [pair (if (identical? vector pair) ::default pair)]
384 | (fn [rf]
385 | (let [mrf (multiplexable rf)
386 | make-rf (cond
387 | (nil? pair) (constantly mrf)
388 | (= ::default pair)
389 | (fn [k] (fn ([acc] acc) ([acc v] (mrf acc k v))))
390 | :else (fn [k] (fn ([acc] acc) ([acc v] (mrf acc (pair k v))))))
391 | m (volatile! (transient {}))]
392 | (if (and (nil? kfn) (nil? vfn))
393 | (kvrf self
394 | ([] (rf))
395 | ([acc] (let-complete [m m] (rf (core/reduce (fn [acc krf] (krf acc)) acc (core/vals (persistent! m))))))
396 | ([acc k v]
397 | (let [krf (or (get @m k) (doto (xform (make-rf k)) (->> (vswap! m assoc! k))))
398 | acc (krf acc v)]
399 | (if (reduced? acc)
400 | (if (reduced? @acc)
401 | (do
402 | (vreset! m (transient {})) ; no need to run completions
403 | @acc) ; downstream is done, propagate
404 | (do
405 | (vswap! m assoc! k nop-rf)
406 | (krf @acc))) ; TODO think again
407 | acc))))
408 | (let [kfn (or kfn key')
409 | vfn (or vfn val')]
410 | (kvrf self
411 | ([] (rf))
412 | ([acc] (let-complete [m m] (rf (core/reduce (fn [acc krf] (krf acc)) acc (core/vals (persistent! m))))))
413 | ([acc x]
414 | (let [k (kfn x)
415 | krf (or (get @m k) (doto (xform (make-rf k)) (->> (vswap! m assoc! k))))
416 | acc (krf acc (vfn x))]
417 | (if (reduced? acc)
418 | (if (reduced? @acc)
419 | (do
420 | (vreset! m (transient {})) ; no need to run completions
421 | @acc) ; downstream is done, propagate
422 | (do
423 | (vswap! m assoc! k nop-rf)
424 | (krf @acc)))
425 | acc)))))))))))
426 |
427 | (defn into-by-key
428 | "A shorthand for the common case (comp (x/by-key ...) (x/into coll))."
429 | [coll & by-key-args]
430 | (comp (apply by-key by-key-args) (into coll)))
431 |
432 | (macros/replace
433 | [#?(:cljd {(java.util.ArrayDeque. n) (dart:coll/Queue)
434 | .add .add
435 | .poll .removeFirst
436 | .size .-length})
437 | #?(:cljs {(java.util.ArrayDeque. n) (Queue.)
438 | .add .enqueue
439 | .poll .dequeue
440 | .size .getCount})
441 | #?(:clj {(.getValues dq) dq})]
442 |
443 | (defn partition
444 | "Returns a partitioning transducer. Each partition is independently transformed using the xform transducer."
445 | ([n]
446 | (partition n n (into [])))
447 | ([n step-or-xform]
448 | (if (fn? step-or-xform)
449 | (partition n n step-or-xform)
450 | (partition n step-or-xform (into []))))
451 | ([#?(:cljd ^int n :default ^long n) step pad-or-xform]
452 | (if (fn? pad-or-xform)
453 | (let [xform pad-or-xform]
454 | (fn [rf]
455 | (let [mxrf (multiplexable rf)
456 | dq (java.util.ArrayDeque. n)
457 | barrier (volatile! n)
458 | xform (comp (map #(if (identical? dq %) nil %)) xform)]
459 | (fn
460 | ([] (rf))
461 | ([acc] (.clear dq) (rf acc))
462 | ([acc x]
463 | (let [b (vswap! barrier dec)]
464 | (when (< b n) (.add dq (if (nil? x) dq x)))
465 | (if (zero? b)
466 | ; this transduce may return a reduced because of mxrf wrapping reduceds coming from rf
467 | (let [acc (transduce xform mxrf acc (.getValues dq))]
468 | (dotimes [_ (core/min n step)] (.poll dq))
469 | (vswap! barrier + step)
470 | acc)
471 | acc)))))))
472 | (partition n step pad-or-xform (into []))))
473 | ([#?(:cljd ^int n :default ^long n) step pad xform]
474 | (fn [rf]
475 | (let [mxrf (multiplexable rf)
476 | dq (java.util.ArrayDeque. n)
477 | barrier (volatile! n)
478 | xform (comp (map #(if (identical? dq %) nil %)) xform)]
479 | (fn
480 | ([] (rf))
481 | ([acc] (if (< @barrier n)
482 | (let [xform (comp cat (take n) xform)
483 | ; don't use mxrf for completion: we want completion and don't want reduced-wrapping
484 | acc (transduce xform rf acc [(.getValues dq) pad])]
485 | (vreset! barrier n)
486 | (.clear dq)
487 | acc)
488 | (rf acc)))
489 | ([acc x]
490 | (let [b (vswap! barrier dec)]
491 | (when (< b n) (.add dq (if (nil? x) dq x)))
492 | (if (zero? b)
493 | ; this transduce may return a reduced because of mxrf wrapping reduceds coming from rf
494 | (let [acc (core/transduce xform mxrf acc (.getValues dq))]
495 | (dotimes [_ (core/min n step)] (.poll dq))
496 | (vswap! barrier + step)
497 | acc)
498 | acc))))))))
499 |
500 | #_(defn zip [xform1 xform2]
501 | (fn [rf]
502 | (let )))
503 |
504 | (defn take-last [#?(:cljd ^int n :default ^long n)]
505 | (fn [rf]
506 | (let [dq (java.util.ArrayDeque. n)]
507 | (fn
508 | ([] (rf))
509 | ([acc] (transduce (map #(if (identical? dq %) nil %)) rf acc (.getValues dq)))
510 | ([acc x]
511 | (.add dq (if (nil? x) dq x))
512 | (when (< n (.size dq)) (.poll dq))
513 | acc)))))
514 |
515 | (defn drop-last
516 | ([] (drop-last 1))
517 | ([#?(:cljd ^int n :default ^long n)]
518 | (fn [rf]
519 | (let [dq (java.util.ArrayDeque. n)
520 | xform (map #(if (identical? dq %) nil %))
521 | rf (xform rf)]
522 | (fn
523 | ([] (rf))
524 | ([acc] (rf acc))
525 | ([acc x]
526 | (.add dq (if (nil? x) dq x))
527 | (if (< n (.size dq))
528 | (rf acc (.poll dq))
529 | acc)))))))
530 |
531 | )
532 |
533 | #?(:cljs
534 | (defn ^:private fn->comparator
535 | "Given a fn that might be boolean valued or a comparator,
536 | return a fn that is a comparator.
537 |
538 | Copied from cljs.core: https://github.com/clojure/clojurescript/blob/95c5cf384a128503b072b7b1916af1a1d5c8871c/src/main/cljs/cljs/core.cljs#L2459-L2471"
539 | [f]
540 | (if (= f compare)
541 | compare
542 | (fn [x y]
543 | (let [r (f x y)]
544 | (if (number? r)
545 | r
546 | (if r
547 | -1
548 | (if (f y x) 1 0))))))))
549 |
550 | (defn sort
551 | ([] (sort compare))
552 | ([cmp]
553 | (fn [rf]
554 | (let [buf #?(:cljd #dart [] :clj (java.util.ArrayList.) :cljs #js [])]
555 | (fn
556 | ([] (rf))
557 | ([acc] (rf (core/reduce rf acc (doto buf #?(:cljd (.sort (dart-comparator cmp))
558 | :clj (java.util.Collections/sort cmp)
559 | :cljs (.sort (fn->comparator cmp)))))))
560 | ([acc x] (#?(:cljd .add :clj .add :cljs .push) buf x) acc))))))
561 |
562 | (defn sort-by
563 | ([kfn] (sort-by kfn compare))
564 | ([kfn cmp]
565 | (sort (fn [a b]
566 | #?(:cljd (cmp (kfn a) (kfn b))
567 | :clj (.compare ^java.util.Comparator cmp (kfn a) (kfn b))
568 | :cljs (cmp (kfn a) (kfn b)))))))
569 |
570 | (defn reductions
571 | "Transducer version of reductions. There's a difference in behavior when init is not provided: (f) is used.
572 | So x/reductions works like x/reduce or transduce, not like reduce and reductions when no init and 1-item input."
573 | ([f] (reductions f (f)))
574 | ([f init]
575 | (fn [rf]
576 | (let [prev (volatile! nil)]
577 | (vreset! prev prev) ; cheap sentinel to detect the first call, this is done to avoid having a 1-item delay
578 | (fn
579 | ([] (rf)) ; no you can't emit init there since there's no guarantee that this arity is going to be called
580 | ([acc] (if (identical? @prev prev)
581 | (rf (unreduced (rf acc init)))
582 | (rf acc)))
583 | ([acc x]
584 | (if (identical? @prev prev)
585 | (let [acc (rf acc (vreset! prev init))]
586 | (if (reduced? acc)
587 | acc
588 | (recur acc x)))
589 | (let [curr (vswap! prev f x)]
590 | (if (reduced? curr)
591 | (ensure-reduced (rf acc @curr))
592 | (rf acc curr))))))))))
593 |
594 | (def avg (reduce rf/avg))
595 | (def sd (reduce rf/sd))
596 |
597 | (defn window
598 | "Returns a transducer which computes an accumulator over the last n items
599 | using two functions: f and its inverse invf.
600 |
601 | The accumulator is initialized with (f).
602 | It is updated to (f (invf acc out) in) where \"acc\" is the current value,
603 | \"in\" the new item entering the window, \"out\" the item exiting the window.
604 | The value passed to the dowstream reducing function is (f acc) enabling acc to be
605 | mutable and 1-arity f to project its state to a value.
606 |
607 | If you don't want to see the accumulator until the window is full then you need to
608 | use (drop (dec n)) to remove them.
609 |
610 | If you don't have an inverse function, consider using partition and reduce:
611 | (x/partition 4 (x/reduce rf))"
612 | [n f invf]
613 | (fn [rf]
614 | (let [ring (object-array n)
615 | vi (volatile! (- n))
616 | vwacc (volatile! (f))]
617 | (fn
618 | ([] (rf))
619 | ([acc] (rf acc))
620 | ([acc x]
621 | (let [i @vi
622 | wacc @vwacc] ; window accumulator
623 | (if (neg? i) ; not full yet
624 | (do
625 | (aset ring (+ n i) x)
626 | (vreset! vi (inc i))
627 | (rf acc (f (vreset! vwacc (f wacc x)))))
628 | (let [x' (aget ring i)]
629 | (aset ring i x)
630 | (vreset! vi (let [i (inc i)] (if (= n i) 0 i)))
631 | (rf acc (f (vreset! vwacc (f (invf wacc x') x))))))))))))
632 |
633 | #?(:cljd nil
634 | :clj
635 | (defn iterator
636 | "Iterator transducing context, returns an iterator on the transformed data.
637 | Equivalent to (.iterator (eduction xform (iterator-seq src-iterator))) except there's is no buffering on values (as in iterator-seq),
638 | This buffering may cause problems when mutable objects are returned by the src-iterator."
639 | ^java.util.Iterator [xform ^java.util.Iterator src-iterator]
640 | (let [NULL (Object.)
641 | dq (java.util.ArrayDeque. 32)
642 | rf (xform (fn ([acc] acc) ([acc x] (.push dq (if (some? x) x NULL)) acc)))
643 | vopen (volatile! true)
644 | ensure-next #(or (some? (.peek dq))
645 | (and @vopen
646 | (if (.hasNext src-iterator)
647 | (let [acc (rf nil (.next src-iterator))]
648 | (when (reduced? acc)
649 | (rf nil)
650 | (vreset! vopen false))
651 | (recur))
652 | (do
653 | (rf nil)
654 | (vreset! vopen false)
655 | (recur)))))]
656 | (reify java.util.Iterator
657 | (hasNext [_]
658 | (ensure-next))
659 | (next [_]
660 | (if (ensure-next)
661 | (let [x (.poll dq)]
662 | (if (identical? NULL x) nil x))
663 | (throw (java.util.NoSuchElementException.))))))))
664 |
665 | #?(:cljd nil
666 | :clj
667 | (defn window-by-time
668 | "ALPHA
669 | Returns a transducer which computes a windowed accumulator over chronologically sorted items.
670 |
671 | timef is a function from one item to its scaled timestamp (as a double). The window length is always 1.0
672 | so timef must normalize timestamps. For example if timestamps are in seconds (and under the :ts key),
673 | to get a 1-hour window you have to use (fn [x] (/ (:ts x) 3600.0)) as timef.
674 |
675 | n is the integral number of steps by which the window slides. With a 1-hour window, 4 means that the window slides every 15 minutes.
676 |
677 | f and invf work like in #'window."
678 | ([timef n f]
679 | (window-by-time timef n
680 | (fn
681 | ([] clojure.lang.PersistentQueue/EMPTY)
682 | ([q] (f (core/reduce f (f) q)))
683 | ([q x] (conj q x)))
684 | (fn [q _] (pop q))))
685 | ([timef n f invf]
686 | (let [timef (fn [x] (long (Math/floor (* n (timef x)))))]
687 | (fn [rf]
688 | (let [dq (java.util.ArrayDeque.)
689 | vwacc (volatile! (f))
690 | flush!
691 | (fn [acc ^long from-ts ^long to-ts]
692 | (loop [ts from-ts acc acc wacc @vwacc]
693 | (let [x (.peekFirst dq)]
694 | (cond
695 | (= ts (timef x))
696 | (do
697 | (.pollFirst dq)
698 | (recur ts acc (invf wacc x)))
699 | (= ts to-ts)
700 | (do
701 | (vreset! vwacc wacc)
702 | acc)
703 | :else
704 | (let [acc (rf acc (f wacc))]
705 | (if (reduced? acc)
706 | (do
707 | (vreset! vwacc wacc)
708 | acc)
709 | (recur (inc ts) acc wacc)))))))]
710 | (fn
711 | ([] (rf))
712 | ([acc]
713 | (let [acc (if-not (.isEmpty dq)
714 | (unreduced (rf acc (f @vwacc)))
715 | acc)]
716 | (rf acc)))
717 | ([acc x]
718 | (let [limit (- (timef x) n)
719 | prev-limit (if-some [prev-x (.peekLast dq)]
720 | (- (timef prev-x) n)
721 | limit)
722 | _ (.addLast dq x) ; so dq is never empty for flush!
723 | acc (flush! acc prev-limit limit)]
724 | (when-not (reduced? acc)
725 | (vswap! vwacc f x))
726 | acc)))))))))
727 |
728 | (defn count
729 | "Count the number of items. Either used directly as a transducer or invoked with two args
730 | as a transducing context."
731 | ([rf]
732 | (let [n #?(:cljd (volatile! 0) :clj (java.util.concurrent.atomic.AtomicLong.) :cljs (volatile! 0))]
733 | (fn
734 | ([] (rf))
735 | ([acc] (rf (unreduced (rf acc #?(:cljd @n :clj (.get n) :cljs @n)))))
736 | ([acc _] #?(:cljd (vswap! n inc) :clj (.incrementAndGet n) :cljs (vswap! n inc)) acc))))
737 | ([xform coll]
738 | (transduce (comp xform count) rf/last coll)))
739 |
740 | (defn multiplex
741 | "Returns a transducer that runs several transducers (sepcified by xforms) in parallel.
742 | If xforms is a map, values of the map are transducers and keys are used to tag each
743 | transducer output:
744 | => (into [] (x/multiplex [(map inc) (map dec)]) (range 3))
745 | [1 -1 2 0 3 1] ; no map, no tag
746 | => (into [] (x/multiplex {:up (map inc) :down (map dec)}) (range 3))
747 | [[:up 1] [:down -1] [:up 2] [:down 0] [:up 3] [:down 1]]"
748 | [xforms]
749 | (fn [rf]
750 | (let [mrf (multiplexable (ensure-kvrf rf))
751 | rfs (volatile! (if (map? xforms)
752 | (into {} (for [[k xform] %
753 | :let [xform (comp xform (for [x %] [k x]))]]
754 | [k (xform mrf)])
755 | xforms)
756 | (into #{} (map #(% mrf)) xforms)))
757 | invoke-rfs (if (map? xforms)
758 | (fn [acc invoke]
759 | (reduce-kv
760 | (fn [acc tag rf]
761 | (let [acc (invoke rf acc)]
762 | (if (reduced? acc)
763 | (if (reduced? @acc)
764 | (do
765 | (vreset! rfs nil)
766 | acc) ; downstream is done, propagate
767 | (do (vswap! rfs dissoc tag) (rf @acc)))
768 | acc)))
769 | acc @rfs))
770 | (fn [acc invoke]
771 | (core/reduce
772 | (fn [acc rf]
773 | (let [acc (invoke rf acc)]
774 | (if (reduced? acc)
775 | (if (reduced? @acc)
776 | (do
777 | (vreset! rfs nil)
778 | acc) ; downstream is done, propagate
779 | (do (vswap! rfs disj rf) (rf @acc)))
780 | acc)))
781 | acc @rfs)))]
782 | (kvrf
783 | ([] (rf))
784 | ([acc] (rf (invoke-rfs acc #(%1 %2))))
785 | ([acc x]
786 | (let [acc (invoke-rfs acc #(%1 %2 x))]
787 | (if (zero? (core/count @rfs))
788 | (ensure-reduced acc)
789 | acc)))
790 | ([acc k v]
791 | (let [acc (invoke-rfs acc #(%1 %2 k v))]
792 | (if (zero? (core/count @rfs))
793 | (ensure-reduced acc)
794 | acc)))))))
795 |
796 | (def last (reduce rf/last))
797 |
798 | (defn some
799 | "Process coll through the specified xform and returns the first local true value."
800 | [xform coll]
801 | (transduce xform rf/some nil coll))
802 |
803 | (defn transjuxt
804 | "Performs several transductions over coll at once. xforms-map can be a map or a sequential collection.
805 | When xforms-map is a map, returns a map with the same keyset as xforms-map.
806 | When xforms-map is a sequential collection returns a vector of same length as xforms-map.
807 | Returns a transducer when coll is omitted."
808 | ([xforms-map]
809 | (let [collect-xform (if (map? xforms-map)
810 | (into {})
811 | (reduce (kvrf
812 | ([] (core/reduce (fn [v _] (conj! v nil))
813 | (transient []) (range (core/count xforms-map))))
814 | ([v] (persistent! v))
815 | ([v i x] (assoc! v i x)))))
816 | xforms-map (if (map? xforms-map) xforms-map (zipmap (range) xforms-map))]
817 | (comp
818 | (multiplex (into {} (by-key (map #(comp % (take 1)))) xforms-map))
819 | collect-xform)))
820 | ([xforms-map coll]
821 | (transduce (transjuxt xforms-map) rf/last coll)))
822 |
823 | (macros/replace
824 | [#?(:cljs {(java.util.concurrent.atomic.AtomicLong.) (atom 0)
825 | (System/nanoTime) (system-time)
826 | (.addAndGet at (- t (System/nanoTime))) (swap! at + (- t (system-time)))
827 | (.addAndGet at (- (System/nanoTime) t)) (swap! at + (- (system-time) t))
828 | .size .getCount})]
829 |
830 | #?(:cljd nil
831 | :default
832 | (defn time
833 | "Measures the time spent in this transformation and prints the measured time.
834 | tag-or-f may be either a function of 1 argument (measured time in ms) in which case
835 | this function will be called instead of printing, or tag-or-f will be print before the measured time."
836 | ([xform] (time "Elapsed time" xform))
837 | ([tag-or-f xform]
838 | (let [pt (if (fn? tag-or-f)
839 | tag-or-f
840 | #(println (core/str tag-or-f ": " % " msecs")))]
841 | (fn [rf]
842 | (let [at (java.util.concurrent.atomic.AtomicLong.)
843 | rf
844 | (fn
845 | ([] (rf))
846 | ([acc] (let [t (System/nanoTime)
847 | r (rf acc)]
848 | (.addAndGet at (- t (System/nanoTime)))
849 | r))
850 | ([acc x]
851 | (let [t (System/nanoTime)
852 | r (rf acc x)]
853 | (.addAndGet at (- t (System/nanoTime)))
854 | r)))
855 | rf (xform rf)]
856 | (fn
857 | ([] (rf))
858 | ([acc]
859 | (let [t (System/nanoTime)
860 | r (rf acc)
861 | total (.addAndGet at (- (System/nanoTime) t))]
862 | (pt #?(:clj (* total 1e-6) :cljs total))
863 | r))
864 | ([acc x]
865 | (let [t (System/nanoTime)
866 | r (rf acc x)]
867 | (.addAndGet at (- (System/nanoTime) t))
868 | r))))))))))
869 |
870 | #_(defn rollup
871 | "Roll-up input data along the provided dimensions (which are functions of one input item),
872 | Values of interest are extracted from items using the valfn function and are then summarized
873 | by summary-fn (a reducing function over values returned by valfn or summaries).
874 | Each level of rollup is a map with two keys: :summary and :details."
875 | ([dimensions valfn summary-fn]
876 | (let [[dim & dims] (reverse dimensions)]
877 | (core/reduce
878 | (fn [xform dim]
879 | (comp
880 | (by-key dim xform)
881 | (transjuxt
882 | {:detail (into {})
883 | :summary (comp vals (map :summary) (reduce summary-fn))})))
884 | (comp (by-key dim (map valfn))
885 | (transjuxt
886 | {:detail (into {})
887 | :summary (comp vals (reduce summary-fn))}))
888 | dims)))
889 | ([dimensions valfn summary-fn coll]
890 | (into {} (rollup dimensions valfn summary-fn) coll)))
891 | )
892 |
--------------------------------------------------------------------------------
/src/net/cgrand/xforms/io.clj:
--------------------------------------------------------------------------------
1 | (ns net.cgrand.xforms.io
2 | (:require [clojure.java.io :as io]
3 | [clojure.java.shell :as sh]
4 | [clojure.edn :as edn])
5 | (:import (java.io Reader BufferedReader IOException InputStream OutputStream BufferedWriter Writer PushbackReader InputStreamReader OutputStreamWriter Closeable)
6 | (java.util Arrays List)
7 | (java.util.concurrent ArrayBlockingQueue)
8 | (java.lang ProcessBuilder$Redirect)
9 | (clojure.lang IFn Fn IReduce)))
10 |
11 | (defn keep-opts [m like]
12 | (let [ns (namespace like)]
13 | (into {}
14 | (keep (fn [[k v]]
15 | (when (= ns (or (namespace k) ns))
16 | [(keyword (name k)) v])))
17 | m)))
18 |
19 | (defn lines-in
20 | "Returns a reducible view over the provided input.
21 | Input is read line by line. Coercion of the input is done by io/reader (and opts are passed to it).
22 | Input is automatically closed upon completion or error."
23 | [in & opts]
24 | (let [no-init (Object.)]
25 | (reify IReduce
26 | (reduce [self f] (.reduce self f no-init))
27 | (reduce [self f init]
28 | (with-open [^Reader rdr (apply io/reader in opts)]
29 | (let [^BufferedReader rdr (cond-> rdr (not (instance? BufferedReader rdr)) (BufferedReader.))
30 | init (if (identical? init no-init)
31 | (or (.readLine rdr) (f))
32 | init)]
33 | (loop [state init]
34 | (if-some [line (.readLine rdr)]
35 | (let [state (f state line)]
36 | (if (reduced? state)
37 | (unreduced state)
38 | (recur state)))
39 | state))))))))
40 |
41 | (defn lines-out
42 | "1-2 args: reducing function that writes values serialized to its accumulator (a java.io.BufferedWriter).
43 | 3+ args: transducing context that writes transformed values to the specified output. The output is
44 | coerced to a BufferedWriter by passing out and opts to clojure.java.io/writer. The output is automatically closed.
45 | Returns the writer."
46 | ([w] w)
47 | ([^BufferedWriter w line]
48 | (doto w
49 | (.write (str line))
50 | (.newLine)))
51 | ([out xform coll & opts]
52 | (with-open [^Writer w (apply io/writer out opts)]
53 | (transduce xform lines-out w coll))))
54 |
55 | (defn edn-in
56 | "Returns a reducible view over the provided input.
57 | Input is read form by form. Coercion of the input is done by io/reader.
58 | Input is automatically closed upon completion or error.
59 | Unqualified options are passed to both edn/read and io/writer, options qualified by clojure.java.io
60 | are only passed (once dequalified) to io/writer, options qualified by clojure.edn are only passed to
61 | edn/read"
62 | [in & {:as opts}]
63 | (let [no-init (Object.)]
64 | (reify IReduce
65 | (reduce [self f] (.reduce self f no-init))
66 | (reduce [self f init]
67 | (with-open [^Reader rdr (apply io/reader in (mapcat seq (keep-opts opts ::io/opts)))]
68 | (let [^BufferedReader rdr (cond-> rdr (not (instance? PushbackReader rdr)) PushbackReader.)
69 | opts (assoc (keep-opts opts ::edn/opts) :eof no-init)
70 | init (if (identical? init no-init)
71 | (let [form (edn/read opts rdr)]
72 | (if (identical? no-init form)
73 | (f)
74 | form))
75 | init)]
76 | (loop [state init]
77 | (let [form (edn/read opts rdr)]
78 | (if (identical? no-init form)
79 | state
80 | (let [state (f state form)]
81 | (if (reduced? state)
82 | (unreduced state)
83 | (recur state))))))))))))
84 |
85 | (defn edn-out
86 | "1-2 args: reducing function that writes values serialized as EDN to its accumulator (a java.io.Writer).
87 | 3+ args: transducing context that writes transformed values to the specified output. The output is
88 | coerced to a Writer by passing out and opts to clojure.java.io/writer. The output is automatically closed.
89 | Returns the writer."
90 | ([w] w)
91 | ([^Writer w x]
92 | (binding [*out* w
93 | *print-length* nil
94 | *print-level* nil
95 | *print-dup* false
96 | *print-meta* false
97 | *print-readably* true]
98 | (prn x)
99 | w))
100 | ([out xform coll & opts]
101 | (with-open [^Writer w (apply io/writer out opts)]
102 | (transduce xform edn-out w coll))))
103 |
104 | (defn- stream-spec [x]
105 | (into {:mode :lines :enc "UTF-8"}
106 | (cond (map? x) x (string? x) {:enc x} (keyword? x) {:mode x})))
107 |
108 | (defn sh
109 | "Transducer or reducible view (in this case assumes empty stdin).
110 | Spawns a process (program cmd with optional arguments arg1 ... argN) and pipes data through it.
111 | Options may be:
112 | * :env, an environment variables map, it will be merged with clojure.java.shell/*sh-env* and JVM environment (in decreasing precedence order),
113 | * :dir, the current dir (defaults to clojure.java.shell/*sh-dir* or JVM current dir),
114 | * :in and :out which are maps with keys :mode (:lines (default), :text or :bytes) and :enc (defaults to \"UTF-8\");
115 | encoding applies only for modes :lines or :text; shorthands exist: a single keyword is equivalent to {:mode k :enc \"UTF-8\"},
116 | a single string is equivalent to {:mode :lines, :enc s}.
117 | In :bytes mode, values are bytes array.
118 | In :lines mode, values are strings representing lines without line delimiters.
119 | In :text mode, values are strings."
120 | {:arglists '([cmd arg1 ... argN & opts])}
121 | [& args]
122 | (reify
123 | IReduce
124 | (reduce [self rf]
125 | (reduce rf (eduction self nil))) ; quick way to handle no init
126 | (reduce [self rf init]
127 | (let [xf (self rf)]
128 | (xf init)))
129 | Fn
130 | IFn
131 | (invoke [_ rf]
132 | (let [[cmd [& {:as opts :keys [env in out dir] :or {dir sh/*sh-dir*}}]] (split-with string? args)
133 | env (into (or sh/*sh-env* {}) env)
134 | env (into {} (for [[k v] env] [(name k) (str v)]))
135 | proc (-> ^List (map str cmd) ProcessBuilder.
136 | (.redirectError ProcessBuilder$Redirect/INHERIT)
137 | (doto (-> .environment (.putAll env)))
138 | (.directory (io/as-file dir))
139 | .start)
140 | EOS (Object.)
141 | q (ArrayBlockingQueue. 16)
142 | drain (fn [acc]
143 | (loop [acc acc]
144 | (if-some [x (.poll q)]
145 | (let [acc (if (identical? EOS x) (reduced acc) (rf acc x))]
146 | (if (reduced? acc)
147 | (do
148 | (.destroy proc)
149 | acc)
150 | (recur acc)))
151 | acc)))
152 | in (stream-spec in)
153 | out (stream-spec out)
154 | ^Closeable stdin (cond-> (.getOutputStream proc) (#{:lines :text} (:mode in)) (-> (OutputStreamWriter. ^String (:enc in)) BufferedWriter.))
155 | stdout (cond-> (.getInputStream proc) (#{:lines :text} (:mode out)) (-> (InputStreamReader. ^String (:enc out)) BufferedReader.))
156 | write!
157 | (case (:mode in)
158 | :lines
159 | (fn [x]
160 | (doto ^BufferedWriter stdin
161 | (.write (str x))
162 | .newLine))
163 | :text
164 | (fn [x]
165 | (.write ^BufferedWriter stdin (str x)))
166 | :bytes
167 | (fn [^bytes x]
168 | (.write ^OutputStream stdin x)))]
169 | (-> (case (:mode out)
170 | :lines
171 | #(loop []
172 | (if-some [s (.readLine ^BufferedReader stdout)]
173 | (do (.put q s) (recur))
174 | (.put q EOS)))
175 | :text
176 | #(let [buf (char-array 1024)]
177 | (loop []
178 | (let [n (.read ^BufferedReader stdout buf)]
179 | (if (neg? n)
180 | (.put q EOS)
181 | (do (.put q (String. buf 0 n)) (recur))))))
182 | :bytes
183 | #(let [buf (byte-array 1024)]
184 | (loop []
185 | (let [n (.read ^InputStream stdout buf)]
186 | (if (neg? n)
187 | (.put q EOS)
188 | (do (.put q (Arrays/copyOf buf n)) (recur)))))))
189 | Thread. .start)
190 | (fn
191 | ([] (rf))
192 | ([acc]
193 | (.close stdin)
194 | (loop [acc acc]
195 | (let [acc (drain acc)]
196 | (if (reduced? acc)
197 | (rf (unreduced acc))
198 | (recur acc)))))
199 | ([acc x]
200 | (let [acc (drain acc)]
201 | (try
202 | (when-not (reduced? acc)
203 | (write! x))
204 | acc
205 | (catch IOException e
206 | (ensure-reduced acc))))))))))
--------------------------------------------------------------------------------
/src/net/cgrand/xforms/nodejs/stream.cljs:
--------------------------------------------------------------------------------
1 | (ns net.cgrand.xforms.nodejs.stream)
2 |
3 | (def ^:private Transform (.-Transform (js/require "stream")))
4 |
5 | (defn transformer
6 | "Returns a stream.Transform object that performs the specified transduction.
7 | options is a js object as per stream.Transform -- however :readableObjectMode and :writableObjectMode are set to true by default."
8 | ([xform] (transformer #js {} xform))
9 | ([options xform]
10 | (let [xrf (xform (fn
11 | ([transform] (doto transform .end))
12 | ([transform x]
13 | (when-not (.push transform x)
14 | (throw (js/Error. "Transformer's internal buffer is full, try passing a larger :highWaterMark option.")))
15 | transform)))]
16 | (specify! (Transform. (.assign js/Object #js {:readableObjectMode true
17 | :writableObjectMode true} options))
18 | Object
19 | (_transform [this x _ cb]
20 | (try
21 | (when (reduced? (xrf this x))
22 | (.push this nil))
23 | (cb)
24 | (catch :default err
25 | (cb err))))
26 | (_flush [this cb]
27 | (try
28 | (xrf this)
29 | (cb)
30 | (catch :default err
31 | (cb err))))))))
32 |
--------------------------------------------------------------------------------
/src/net/cgrand/xforms/rfs.cljc:
--------------------------------------------------------------------------------
1 | (ns net.cgrand.xforms.rfs
2 | {:author "Christophe Grand"}
3 | (:refer-clojure :exclude [str last min max some])
4 | #?(:cljs (:require-macros
5 | [net.cgrand.macrovich :as macros]
6 | [net.cgrand.xforms.rfs :refer [or-instance?]])
7 | :clj (:require [net.cgrand.macrovich :as macros]))
8 | (:require [#?(:clj clojure.core :cljs cljs.core) :as core])
9 | #?(:cljd (:require ["dart:math" :as Math]))
10 | #?(:cljs (:import [goog.string StringBuffer])))
11 |
12 | (macros/deftime
13 | (defmacro ^:private or-instance? [class x y]
14 | (let [xsym (gensym 'x_)]
15 | `(let [~xsym ~x]
16 | (if #?(:cljd (dart/is? ~xsym ~class)
17 | :default (instance? ~class ~xsym))
18 | ~(with-meta xsym {:tag class}) ~y)))))
19 |
20 | (declare str!)
21 |
22 | (macros/usetime
23 |
24 | #? (:cljs
25 | (defn ^:private cmp [f a b]
26 | (let [r (f a b)]
27 | (cond
28 | (number? r) r
29 | r -1
30 | (f b a) 1
31 | :else 0))))
32 |
33 | (defn minimum
34 | ([#?(:cljd comparator :clj ^java.util.Comparator comparator :cljs comparator)]
35 | (let [#?@(:cljd [comparator (dart-comparator comparator)] :default [])]
36 | (fn
37 | ([] nil)
38 | ([x] x)
39 | ([a b] (cond
40 | (nil? a) b
41 | (nil? b) a
42 | (pos? #?(:cljd (comparator a b)
43 | :clj (.compare comparator a b)
44 | :cljs (cmp comparator a b))) b
45 | :else a)))))
46 | ([#?(:cljd comparator :clj ^java.util.Comparator comparator :cljs comparator) absolute-maximum]
47 | (let [#?@(:cljd [comparator (dart-comparator comparator)] :default [])]
48 | (fn
49 | ([] ::+infinity)
50 | ([x] (if (#?(:clj identical? :cljs keyword-identical?) ::+infinity x)
51 | absolute-maximum
52 | x))
53 | ([a b]
54 | (if (or
55 | (#?(:clj identical? :cljs keyword-identical?) ::+infinity a)
56 | (pos? #?(:cljd (comparator a b)
57 | :clj (.compare comparator a b)
58 | :cljs (cmp comparator a b))))
59 | b a))))))
60 |
61 | (defn maximum
62 | ([#?(:cljd comparator :clj ^java.util.Comparator comparator :cljs comparator)]
63 | (let [#?@(:cljd [comparator (dart-comparator comparator)] :default [])]
64 | (fn
65 | ([] nil)
66 | ([x] x)
67 | ([a b] (cond
68 | (nil? a) b
69 | (nil? b) a
70 | (neg? #?(:cljd (comparator a b)
71 | :clj (.compare comparator a b)
72 | :cljs (cmp comparator a b))) b
73 | :else a)))))
74 | ([#?(:cljd comparator :clj ^java.util.Comparator comparator :cljs comparator) absolute-minimum]
75 | (let [#?@(:cljd [comparator (dart-comparator comparator)] :default [])]
76 | (fn
77 | ([] ::-infinity)
78 | ([x] (if (#?(:clj identical? :cljs keyword-identical?) ::-infinity x)
79 | absolute-minimum
80 | x))
81 | ([a b]
82 | (if (or (#?(:clj identical? :cljs keyword-identical?) ::-infinity a)
83 | (neg? #?(:cljd (comparator a b)
84 | :clj (.compare comparator a b)
85 | :cljs (cmp comparator a b))))
86 | b a))))))
87 |
88 | (def min (minimum compare))
89 |
90 | (def max (maximum compare))
91 |
92 | (defn avg
93 | "Reducing fn to compute the arithmetic mean."
94 | ([] nil)
95 | ([#?(:cljd ^{:tag #/(List? double)} acc :clj ^doubles acc :cljs ^doubles acc)]
96 | (when acc (/ (aget acc 1) (aget acc 0))))
97 | ([acc x] (avg acc x 1))
98 | ([#?(:cljd ^{:tag #/(List? double)} acc :clj ^doubles acc :cljs ^doubles acc) x w] ; weighted mean
99 | (let [acc (or acc #?(:cljd (double-array 2) :clj (double-array 2) :cljs #js [0.0 0.0]))]
100 | (doto acc
101 | (aset 0 (+ (aget acc 0) w))
102 | (aset 1 (+ (aget acc 1) (* w x)))))))
103 |
104 | (defn sd
105 | "Reducing fn to compute the standard deviation. Returns 0 if no or only one item."
106 | ([] #?(:cljd (double-array 3) :clj (double-array 3) :cljs #js [0.0 0.0 0.0]))
107 | ([#?(:cljd ^{:tag #/(List double)} a :default ^doubles a)]
108 | (let [s (aget a 0) n (aget a 2)]
109 | (if (< 1 n)
110 | (Math/sqrt (/ s (dec n)))
111 | 0.0)))
112 | ([#?(:cljd ^{:tag #/(List double)} a :default ^doubles a) x]
113 | (let [s (aget a 0) m (aget a 1) n (aget a 2)
114 | d (- x m)
115 | n (inc n)
116 | m' (+ m (/ d n))]
117 | (doto a
118 | (aset 0 (+ s (* d (- x m'))))
119 | (aset 1 m')
120 | (aset 2 n)))))
121 |
122 | (defn last
123 | "Reducing function that returns the last value."
124 | ([] nil)
125 | ([x] x)
126 | ([_ x] x))
127 |
128 | (defn some
129 | "Reducing function that returns the first logical true value."
130 | ([] nil)
131 | ([x] x)
132 | ([_ x] (when x (reduced x))))
133 |
134 | (defn str!
135 | "Like xforms/str but returns a StringBuilder."
136 | ([] (#?(:cljd StringBuffer :clj StringBuilder. :cljs StringBuffer.)))
137 | ([sb] (or-instance? #?(:cljd StringBuffer :clj StringBuilder :cljs StringBuffer) sb
138 | (#?(:cljd StringBuffer :clj StringBuilder. :cljs StringBuffer.) (core/str sb))))
139 | ; the instance? checks are for compatibility with str in case of seeded reduce/transduce.
140 | ([sb x] (doto (or-instance?
141 | #?(:cljd StringBuffer :clj StringBuilder :cljs StringBuffer) sb
142 | (#?(:cljd StringBuffer :clj StringBuilder. :cljs StringBuffer.) (core/str sb)))
143 | (#?(:cljd .write :default .append) x))))
144 |
145 | (def str
146 | "Reducing function to build strings in linear time. Acts as replacement for clojure.core/str in a reduce/transduce call."
147 | (completing str! core/str))
148 |
149 | #_(defn juxt
150 | "Returns a reducing fn which compute all rfns at once and whose final return
151 | value is a vector of the final return values of each rfns."
152 | [& rfns]
153 | (let [rfns (mapv ensure-kvrf rfns)]
154 | (kvrf
155 | ([] (mapv #(vector % (volatile! (%))) rfns))
156 | ([acc] (mapv (fn [[rf vacc]] (rf (unreduced @vacc))) acc))
157 | ([acc x]
158 | (let [some-unreduced (core/reduce (fn [some-unreduced [rf vacc]]
159 | (when-not (reduced? @vacc) (vswap! vacc rf x) true))
160 | false acc)]
161 | (if some-unreduced acc (reduced acc))))
162 | ([acc k v]
163 | (let [some-unreduced (core/reduce (fn [some-unreduced [rf vacc]]
164 | (when-not (reduced? @vacc) (vswap! vacc rf k v) true))
165 | false acc)]
166 | (if some-unreduced acc (reduced acc)))))))
167 |
168 | #_(defn juxt-map
169 | [& key-rfns]
170 | (let [f (apply juxt (take-nth 2 (next key-rfns)))
171 | keys (vec (take-nth 2 key-rfns))]
172 | (let [f (ensure-kvrf f)]
173 | (kvrf
174 | ([] (f))
175 | ([acc] (zipmap keys (f acc)))
176 | ([acc x] (f acc x))
177 | ([acc k v] (f acc k v))))))
178 | )
179 |
--------------------------------------------------------------------------------
/test/net/cgrand/xforms_test.cljc:
--------------------------------------------------------------------------------
1 | (ns net.cgrand.xforms-test
2 | (:refer-clojure :exclude [partition reductions])
3 | (:require [clojure.test :refer [are is deftest testing]]
4 | [net.cgrand.xforms :as x]))
5 |
6 | (defn trial
7 | "A transducing context for testing that transducers are well-behaved towards
8 | linear use of the accumulator, init, completion and handling of reduced values.
9 | A \"poisonous\" reducing function rf is passed to the transducer.
10 | n is the number of calls to rf before it returns a reduced.
11 | accs is a collection of successive return values for rf."
12 | ([xform n coll]
13 | (trial xform n (repeatedly #(#?(:clj Object. :cljs js/Object.))) coll))
14 | ([xform n accs coll]
15 | (let [vaccs (volatile! accs)
16 | vstate (volatile! {:n n :acc (first @vaccs) :state :init})
17 | check-acc (fn [acc]
18 | (when (reduced? acc)
19 | (throw (ex-info "Called with reduced accumulator" (assoc @vstate :actual-acc acc))))
20 | (when-not (identical? acc (:acc @vstate))
21 | (throw (ex-info "Called with an unexpected accumulator (either non-linear or out of thin air)" (assoc @vstate :actual-acc acc)))))
22 | rf (fn
23 | ([]
24 | (when-not (= :init (:state @vstate))
25 | (throw (ex-info "Init arity called on a started or completed rf." @vstate)))
26 | (:acc (vswap! vstate assoc :state :started)))
27 | ([acc]
28 | (when (= :completed (:state @vstate))
29 | (throw (ex-info "Completion arity called on an already completed rf." @vstate)))
30 | (check-acc acc)
31 | (:acc (vswap! vstate assoc :state :completed :acc (first (vswap! vaccs next)))))
32 | ([acc x]
33 | (when (= :completed (:state @vstate))
34 | (throw (ex-info "Step arity called on an already completed rf." @vstate)))
35 | (when (= :reduced (:state @vstate))
36 | (throw (ex-info "Step arity called on a reduced rf." @vstate)))
37 | (check-acc acc)
38 | (let [n (:n @vstate)]
39 | (let [acc (first (vswap! vaccs next))]
40 | (if (pos? n)
41 | (:acc (vswap! vstate assoc :acc acc :n (dec n)))
42 | (reduced (:acc (vswap! vstate assoc :acc acc :state :reduced))))))))
43 | res (transduce xform rf coll)]
44 | (check-acc res)
45 | (when-not (= :completed (:state @vstate))
46 | (throw (ex-info "Completion arity never called" @vstate)))
47 | true)))
48 |
49 | (deftest proper-rf-usage
50 | (testing "Ensuring that reducing functions returned by transducers are well-behaved."
51 | (is (trial (x/by-key odd? identity)
52 | 4 (range 16)))
53 | (is (trial (x/by-key odd? identity nil identity)
54 | 4 (range 16)))
55 | (is (trial (x/by-key odd? (take 2))
56 | 8 (range 16)))
57 | (is (trial (x/by-key odd? identity)
58 | 8 (range 2)))
59 | (is (trial (x/partition 3 identity)
60 | 4 (range 16)))
61 | (is (trial (x/partition 3 (take 2))
62 | 8 (range 16)))
63 | (is (trial (x/partition 3 (take 2))
64 | 8 (range 2)))
65 | (is (trial (x/reductions conj [])
66 | 8 (range 2)))
67 | (is (trial (x/reductions conj)
68 | 8 (range 2)))
69 | (is (trial (x/into [])
70 | 4 (range 16)))
71 | (is (trial (x/for [x % y (range x)] [x y])
72 | 4 (range 16)))
73 | (is (trial (x/reduce +)
74 | 4 (range 16)))))
75 |
76 | (deftest reductions
77 | (is (= (into [] (x/reductions +) (range 10)) [0 0 1 3 6 10 15 21 28 36 45]))
78 | (is (= (into [] (x/reductions +) (range 0)) [0]))
79 | (is (= (into [] (x/reductions +) (range 1)) [0 0]))
80 | (is (= (into [] (x/reductions +) (range 2)) [0 0 1]))
81 | (is (= (into [] (comp (x/reductions +) (take 2)) (range)) [0 0]))
82 | (is (= (into [] (comp (x/reductions +) (take 3)) (range)) [0 0 1]))
83 | (is (= (into [] (comp (take 3) (x/reductions +)) (range)) [0 0 1 3]))
84 | (is (= (into [] (x/reductions (constantly (reduced 42)) 0) (range)) [0 42])))
85 |
86 | (deftest partition
87 | (is (= (into [] (x/partition 2 1 nil (x/into [])) (range 8))
88 | [[0 1] [1 2] [2 3] [3 4] [4 5] [5 6] [6 7] [7]]))
89 | (is (= (into [] (x/partition 2 1 (x/into [])) (range 8))
90 | [[0 1] [1 2] [2 3] [3 4] [4 5] [5 6] [6 7]]))
91 | (is (= (into [] (comp (x/partition 2 2 nil) (x/into [])) (range 8))
92 | [[[0 1] [2 3] [4 5] [6 7]]])))
93 |
94 | (deftest without
95 | (is (= {0 :ok 2 :ok 4 :ok 6 :ok 8 :ok} (x/without (zipmap (range 10) (repeat :ok)) (filter odd?) (range 20))))
96 | (is (= #{0 2 4 6 8 } (x/without (set (range 10)) (filter odd?) (range 20)))))
97 |
98 | #?(:bb nil ;; babashka doesn't currently support calling iterator on range type
99 | :clj
100 | (do
101 | (deftest iterator
102 | (is (true? (.hasNext (x/iterator x/count (.iterator ^java.lang.Iterable (range 5))))))
103 | (is (is (= [5] (iterator-seq (x/iterator x/count (.iterator ^java.lang.Iterable (range 5)))))))
104 | (is (= [[0 1] [1 2] [2 3] [3 4] [4]] (iterator-seq (x/iterator (x/partition 2 1 nil) (.iterator ^java.lang.Iterable (range 5)))))))
105 |
106 | (deftest window-by-time
107 | (is (= (into
108 | []
109 | (x/window-by-time :ts 4
110 | (fn
111 | ([] clojure.lang.PersistentQueue/EMPTY)
112 | ([q] (vec q))
113 | ([q x] (conj q x)))
114 | (fn [q _] (pop q)))
115 | (map (fn [x] {:ts x}) (concat (range 0 2 0.5) (range 3 5 0.25))))
116 | [[{:ts 0}] ; t = 0
117 | [{:ts 0}] ; t = 0.25
118 | [{:ts 0} {:ts 0.5}] ; t = 0.5
119 | [{:ts 0} {:ts 0.5}] ; t = 0.75
120 | [{:ts 0.5} {:ts 1.0}] ; t = 1.0
121 | [{:ts 0.5} {:ts 1.0}] ; t = 1.25
122 | [{:ts 1.0} {:ts 1.5}] ; t = 1.5
123 | [{:ts 1.0} {:ts 1.5}] ; t = 1.75
124 | [{:ts 1.5}] ; t = 2.0
125 | [{:ts 1.5}] ; t = 2.25
126 | [] ; t = 2.5
127 | [] ; t = 2.75
128 | [{:ts 3}] ; t = 3.0
129 | [{:ts 3} {:ts 3.25}] ; t = 3.25
130 | [{:ts 3} {:ts 3.25} {:ts 3.5}] ; t = 3.5
131 | [{:ts 3} {:ts 3.25} {:ts 3.5} {:ts 3.75}] ; t = 3.75
132 | [{:ts 3.25} {:ts 3.5} {:ts 3.75} {:ts 4.0}] ; t = 4.0
133 | [{:ts 3.5} {:ts 3.75} {:ts 4.0} {:ts 4.25}] ; t = 4.25
134 | [{:ts 3.75} {:ts 4.0} {:ts 4.25} {:ts 4.5}] ; t = 4.5
135 | [{:ts 4.0} {:ts 4.25} {:ts 4.5} {:ts 4.75}]]))))) ; t = 4.75
136 |
137 | (deftest do-not-kvreduce-vectors
138 | (is (= {0 nil 1 nil} (x/into {} (x/for [[k v] %] [k v]) [[0] [1]])))
139 | (is (= {0 nil 1 nil} (x/into {} (x/for [_ % [k v] [[0] [1]]] [k v]) ["a"]))))
140 |
141 | (deftest sorting
142 | (is (= (range 100) (x/into [] (x/sort) (shuffle (range 100)))))
143 | (is (= (range 100) (x/into [] (x/sort <) (shuffle (range 100)))))
144 | (is (= (reverse (range 100)) (x/into [] (x/sort >) (shuffle (range 100)))))
145 | (is (= (sort-by str (range 100)) (x/into [] (x/sort-by str) (shuffle (range 100)))))
146 | (is (= (sort-by str (comp - compare) (range 100)) (x/into [] (x/sort-by str (comp - compare)) (shuffle (range 100)))))
147 | (is (= (sort-by identity > (shuffle (range 100))) (x/into [] (x/sort-by identity >) (shuffle (range 100))))))
148 |
149 | (deftest destructuring-pair?
150 | (let [destructuring-pair? #'x/destructuring-pair?]
151 | (are [candidate expected]
152 | (= expected (destructuring-pair? candidate))
153 | '[a b] true
154 | '[a b c] false
155 | '[& foo] false
156 | '[:as foo] false
157 | 1 false
158 | '(a b) false
159 | '{foo bar} false
160 | '{foo :bar} false)))
161 |
162 | (defmacro wraps-for-with-no-destructuring []
163 | (x/into [] (x/for [x (range 5)] x)))
164 |
165 | (deftest for-in-macro
166 | (is (= [0 1 2 3 4] (wraps-for-with-no-destructuring))))
167 |
--------------------------------------------------------------------------------
/tests.edn:
--------------------------------------------------------------------------------
1 | #kaocha/v1 {}
2 |
--------------------------------------------------------------------------------