diff --git a/README.md b/README.md index a7629ce..91df37a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ More transducers and reducing functions for Clojure! [![Build Status](https://travis-ci.org/cgrand/xforms.png?branch=master)](https://travis-ci.org/cgrand/xforms) -Transducers: `reduce`, `into`, `last`, `count`, `avg`, `min`, `minimum`, `max`, `maximum`, `str`, `by-key`, `partition`, `for`, `multiplex`, `transjuxt`, `window` and `window-by-time`. +Transducers: `reduce`, `into`, `last`, `count`, `avg`, `min`, `minimum`, `max`, `maximum`, `str`, `by-key`, `partition`, `reductions`, `for`, `multiplex`, `transjuxt`, `window` and `window-by-time`. Reducing functions (in `net.cgrand.xforms.rfs`): `min`, `minimum`, `max`, `maximum`, `str`, `str!`, `avg`, `juxt` and `last`. @@ -15,7 +15,7 @@ Transducing context: `transjuxt` (for performing several transductions in a sing Add this dependency to your project: ```clj -[net.cgrand/xforms "0.5.1"] +[net.cgrand/xforms "0.6.0"] ``` ```clj @@ -199,6 +199,11 @@ Evaluation count : 24 in 6 samples of 4 calls. Execution time std-deviation : 364,932996 µs ``` +## Changelog +### 0.6.0 + + * Added `x/reductions` + * 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. ## License diff --git a/project.clj b/project.clj index cc62094..f3cf12e 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject net.cgrand/xforms "0.5.1" +(defproject net.cgrand/xforms "0.6.0" :description "Extra transducers for Clojure" #_#_:url "http://example.com/FIXME" :license {:name "Eclipse Public License" diff --git a/src/net/cgrand/xforms.clj b/src/net/cgrand/xforms.clj index 50fa823..542884a 100644 --- a/src/net/cgrand/xforms.clj +++ b/src/net/cgrand/xforms.clj @@ -1,16 +1,17 @@ (ns net.cgrand.xforms "Extra transducers for Clojure" {:author "Christophe Grand"} - (:refer-clojure :exclude [reduce into count for partition str last keys vals min max]) + (:refer-clojure :exclude [reduce reductions into count for partition str last keys vals min max]) (:require [clojure.core :as clj] [net.cgrand.xforms.rfs :as rf])) (defmacro for - "Like clojure.core/for with the first expression being replaced by % (or _). Returns a transducer." + "Like clojure.core/for with the first expression being replaced by % (or _). Returns a transducer. + When the first expression is not % (or _) returns an eduction." [[binding %or_ & seq-exprs] body-expr] - (assert (and (symbol? %or_) (#{"%" "_"} (name %or_))) - "The second element of the comprehension vector must be % or _.") - (let [rf (gensym 'rf) + (if-not (and (symbol? %or_) (#{"%" "_"} (name %or_))) + `(eduction (for [~binding ~'% ~@seq-exprs] ~body-expr) ~%or_) + (let [rf (gensym 'rf) acc (gensym 'acc) pair? #(and (vector? %) (= 2 (clj/count %))) destructuring-pair? (every-pred pair? @@ -47,7 +48,7 @@ ~(if (destructuring-pair? binding) `([~acc ~@(map #(vary-meta % dissoc :tag) binding)] ~body) `([~acc k# v#] - (let [~binding (clojure.lang.MapEntry. k# v#)] ~body)))))))) + (let [~binding (clojure.lang.MapEntry. k# v#)] ~body))))))))) (defprotocol KvRfable "Protocol for reducing fns that accept key and val as separate arguments." (some-kvrf [f] "Returns a kvrf or nil")) @@ -310,6 +311,30 @@ acc) acc)))))))) +(defn reductions + "Transient version of reductions. There's a difference in behavior when init is not provided: (f) is used. + So x/reductions works like x/reduce or transduce, not like reduce and reductions when no init and 1-item input." + ([f] (reductions f (f))) + ([f init] + (fn [rf] + (let [prev (volatile! nil)] + (vreset! prev prev) ; cheap sentinel to detect the first call, this is done to avoid having a 1-item delay + (fn + ([] (rf)) ; no you can't emit init there since there's no guarantee that this arity is going to be called + ([acc] (if (identical? @prev prev) + (rf (unreduced (rf acc init))) + (rf acc))) + ([acc x] + (if (identical? @prev prev) + (let [acc (rf acc (vreset! prev init))] + (if (reduced? acc) + acc + (recur acc x))) + (let [curr (vswap! prev f x)] + (if (reduced? curr) + (ensure-reduced (rf acc @curr)) + (rf acc curr)))))))))) + (def avg (reduce rf/avg)) (defn window diff --git a/test/net/cgrand/xforms_test.clj b/test/net/cgrand/xforms_test.clj index 32d898e..f83aef7 100644 --- a/test/net/cgrand/xforms_test.clj +++ b/test/net/cgrand/xforms_test.clj @@ -61,6 +61,10 @@ 8 (range 16))) (is (trial (x/partition 3 (take 2)) 8 (range 2))) + (is (trial (x/reductions conj []) + 8 (range 2))) + (is (trial (x/reductions conj) + 8 (range 2))) (is (trial (x/into []) 4 (range 16))) (is (trial (x/for [x % y (range x)] [x y]) @@ -68,6 +72,16 @@ (is (trial (x/reduce +) 4 (range 16))))) +(deftest reductions + (is (= (into [] (x/reductions +) (range 10)) [0 0 1 3 6 10 15 21 28 36 45])) + (is (= (into [] (x/reductions +) (range 0)) [0])) + (is (= (into [] (x/reductions +) (range 1)) [0 0])) + (is (= (into [] (x/reductions +) (range 2)) [0 0 1])) + (is (= (into [] (comp (x/reductions +) (take 2)) (range)) [0 0])) + (is (= (into [] (comp (x/reductions +) (take 3)) (range)) [0 0 1])) + (is (= (into [] (comp (take 3) (x/reductions +)) (range)) [0 0 1 3])) + (is (= (into [] (x/reductions (constantly (reduced 42)) 0) (range)) [0 42]))) + (deftest window-by-time (is (= (into []