Skip to content

Commit a180568

Browse files
committed
Added the 'function' namespace, for higher order functions based on records.
1 parent 3eec718 commit a180568

File tree

4 files changed

+269
-2
lines changed

4 files changed

+269
-2
lines changed

src/active/clojure/function.cljc

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
(ns active.clojure.function
2+
"Redefines higher order functions and function combinators from
3+
clojure.core via applicable records (ifn? but not fn?). The
4+
advantage is, that those objects compare = if they are created from
5+
equal arguments. Disadvantages are that they are probably a bit
6+
slower. They also don't implement some additional protocols like
7+
Runnable yet."
8+
(:refer-clojure :exclude [partial constantly comp complement juxt fnil every-pred some-fn bound-fn*
9+
completing]))
10+
11+
#?(:cljs
12+
(defrecord ^:no-doc Partial [f_ args]
13+
IFn
14+
(-invoke [this] (apply f_ args))
15+
(-invoke [this a] (apply f_ (concat args (list a))))
16+
(-invoke [this a b] (apply f_ (concat args (list a b))))
17+
(-invoke [this a b c] (apply f_ (concat args (list a b c))))
18+
(-invoke [this a b c d] (apply f_ (concat args (list a b c d))))
19+
(-invoke [this a b c d e] (apply f_ (concat args (list a b c d e))))
20+
(-invoke [this a b c d e f] (apply f_ (concat args (list a b c d e f))))
21+
(-invoke [this a b c d e f g] (apply f_ (concat args (list a b c d e f g))))
22+
(-invoke [this a b c d e f g h] (apply f_ (concat args (list a b c d e f g h))))
23+
(-invoke [this a b c d e f g h i] (apply f_ (concat args (list a b c d e f g h i))))
24+
(-invoke [this a b c d e f g h i j] (apply f_ (concat args (list a b c d e f g h i j))))
25+
(-invoke [this a b c d e f g h i j k] (apply f_ (concat args (list a b c d e f g h i j k))))
26+
(-invoke [this a b c d e f g h i j k l] (apply f_ (concat args (list a b c d e f g h i j k l))))
27+
(-invoke [this a b c d e f g h i j k l m] (apply f_ (concat args (list a b c d e f g h i j k l m))))
28+
(-invoke [this a b c d e f g h i j k l m n] (apply f_ (concat args (list a b c d e f g h i j k l m n))))
29+
(-invoke [this a b c d e f g h i j k l m n o] (apply f_ (concat args (list a b c d e f g h i j k l m n o))))
30+
(-invoke [this a b c d e f g h i j k l m n o p] (apply f_ (concat args (list a b c d e f g h i j k l m n o p))))
31+
(-invoke [this a b c d e f g h i j k l m n o p q] (apply f_ (concat args (list a b c d e f g h i j k l m n o p q))))
32+
(-invoke [this a b c d e f g h i j k l m n o p q r] (apply f_ (concat args (list a b c d e f g h i j k l m n o p q r))))
33+
(-invoke [this a b c d e f g h i j k l m n o p q r s] (apply f_ (concat args (list a b c d e f g h i j k l m n o p q r s))))
34+
(-invoke [this a b c d e f g h i j k l m n o p q r s t] (apply f_ (concat args (list a b c d e f g h i j k l m n o p q r s t))))
35+
(-invoke [this a b c d e f g h i j k l m n o p q r s t rest] (apply f_ (concat args (list a b c d e f g h i j k l m n o p q r s t) rest)))))
36+
37+
#?(:clj
38+
(defrecord ^:no-doc Partial [f_ args]
39+
clojure.lang.IFn
40+
(applyTo [this arglist] (apply f_ (concat args arglist)))
41+
(invoke [this] (apply f_ args))
42+
(invoke [this a] (apply f_ (concat args (list a))))
43+
(invoke [this a b] (apply f_ (concat args (list a b))))
44+
(invoke [this a b c] (apply f_ (concat args (list a b c))))
45+
(invoke [this a b c d] (apply f_ (concat args (list a b c d))))
46+
(invoke [this a b c d e] (apply f_ (concat args (list a b c d e))))
47+
(invoke [this a b c d e f] (apply f_ (concat args (list a b c d e f))))
48+
(invoke [this a b c d e f g] (apply f_ (concat args (list a b c d e f g))))
49+
(invoke [this a b c d e f g h] (apply f_ (concat args (list a b c d e f g h))))
50+
(invoke [this a b c d e f g h i] (apply f_ (concat args (list a b c d e f g h i))))
51+
(invoke [this a b c d e f g h i j] (apply f_ (concat args (list a b c d e f g h i j))))
52+
(invoke [this a b c d e f g h i j k] (apply f_ (concat args (list a b c d e f g h i j k))))
53+
(invoke [this a b c d e f g h i j k l] (apply f_ (concat args (list a b c d e f g h i j k l))))
54+
(invoke [this a b c d e f g h i j k l m] (apply f_ (concat args (list a b c d e f g h i j k l m))))
55+
(invoke [this a b c d e f g h i j k l m n] (apply f_ (concat args (list a b c d e f g h i j k l m n))))
56+
(invoke [this a b c d e f g h i j k l m n o] (apply f_ (concat args (list a b c d e f g h i j k l m n o))))
57+
(invoke [this a b c d e f g h i j k l m n o p] (apply f_ (concat args (list a b c d e f g h i j k l m n o p))))
58+
(invoke [this a b c d e f g h i j k l m n o p q] (apply f_ (concat args (list a b c d e f g h i j k l m n o p q))))
59+
(invoke [this a b c d e f g h i j k l m n o p q r] (apply f_ (concat args (list a b c d e f g h i j k l m n o p q r))))
60+
(invoke [this a b c d e f g h i j k l m n o p q r s] (apply f_ (concat args (list a b c d e f g h i j k l m n o p q r s))))
61+
(invoke [this a b c d e f g h i j k l m n o p q r s t] (apply f_ (concat args (list a b c d e f g h i j k l m n o p q r s t))))))
62+
63+
64+
(defn partial
65+
"Takes a function f and fewer than the normal arguments to f, and
66+
returns a fn that takes a variable number of additional args. When
67+
called, the returned function calls f with args + additional args."
68+
[f & args]
69+
(Partial. f args))
70+
71+
(letfn [(_lift-variadic [f fargs & args]
72+
(apply (apply f fargs) args))]
73+
;; Note: this is most easiest way to lift a higher-order fn f, but often not the most efficient
74+
(defn lift-variadic [f & fargs]
75+
(partial _lift-variadic f fargs)))
76+
77+
(letfn [(_constantly [v & args]
78+
v)]
79+
(defn constantly
80+
"Returns a function that takes any number of arguments and returns x."
81+
[v]
82+
(partial _constantly v)))
83+
84+
(defn comp
85+
"Takes a set of functions and returns a fn that is the composition
86+
of those fns. The returned fn takes a variable number of args,
87+
applies the rightmost of fns to the args, the next
88+
fn (right-to-left) to the result, etc."
89+
([] identity)
90+
([f] f)
91+
([f g & fs]
92+
(apply lift-variadic clojure.core/comp f g fs)))
93+
94+
(defn complement
95+
"Takes a fn f and returns a fn that takes the same arguments as f,
96+
has the same effects, if any, and returns the opposite truth value."
97+
[f]
98+
(lift-variadic clojure.core/complement f))
99+
100+
(defn juxt
101+
"Takes a set of functions and returns a fn that is the juxtaposition
102+
of those fns. The returned fn takes a variable number of args, and
103+
returns a vector containing the result of applying each fn to the
104+
args (left-to-right).
105+
((juxt a b c) x) => [(a x) (b x) (c x)]"
106+
[f & fns]
107+
(apply lift-variadic clojure.core/juxt f fns))
108+
109+
(defn fnil
110+
"Takes a function f, and returns a function that calls f, replacing
111+
a nil first argument to f with the supplied value x. Higher arity
112+
versions can replace arguments in the second and third
113+
positions (y, z). Note that the function f can take any number of
114+
arguments, not just the one(s) being nil-patched."
115+
([f x] (lift-variadic clojure.core/fnil f x))
116+
([f x y] (lift-variadic clojure.core/fnil f x y))
117+
([f x y z] (lift-variadic clojure.core/fnil f x y z)))
118+
119+
(defn every-pred
120+
"Takes a set of predicates and returns a function f that returns true if all of its
121+
composing predicates return a logical true value against all of its arguments, else it returns
122+
false. Note that f is short-circuiting in that it will stop execution on the first
123+
argument that triggers a logical false result against the original predicates."
124+
[p & ps]
125+
(apply lift-variadic clojure.core/every-pred p ps))
126+
127+
128+
(defn some-fn
129+
"Takes a set of predicates and returns a function f that returns the first logical true value
130+
returned by one of its composing predicates against any of its arguments, else it returns
131+
logical false. Note that f is short-circuiting in that it will stop execution on the first
132+
argument that triggers a logical true result against the original predicates."
133+
[p & ps]
134+
(apply lift-variadic clojure.core/some-fn p ps))
135+
136+
#?(:clj
137+
(letfn [(_bound-fn* [bindings f & args]
138+
(apply with-bindings* bindings f args))]
139+
(defn bound-fn*
140+
"Returns a function, which will install the same bindings in effect as in
141+
the thread at the time bound-fn* was called and then call f with any given
142+
arguments. This may be used to define a helper function which runs on a
143+
different thread, but needs the same bindings in place."
144+
[f]
145+
;; Note: this cannot be done with lift-variadic, because
146+
;; get-thread-bindings is side-effectful, and has to be called
147+
;; now, not later.
148+
(let [bindings (get-thread-bindings)]
149+
(partial _bound-fn* bindings f)))))
150+
151+
(defn completing
152+
"Takes a reducing function f of 2 args and returns a fn suitable for
153+
transduce by adding an arity-1 signature that calls cf (default -
154+
identity) on the result argument."
155+
([f] (completing f identity))
156+
([f cf] (lift-variadic clojure.core/completing f cf)))
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
(ns active.clojure.function-test
2+
(:require [active.clojure.function :as f]
3+
#?(:clj [active.clojure.function-test-util :as u])
4+
#?(:clj [clojure.test :refer :all])
5+
#?(:cljs [cljs.test :refer-macros [deftest is testing]]))
6+
#?(:cljs (:require-macros [active.clojure.function-test-util :as u])))
7+
8+
(deftest partial-test
9+
(u/generate-tests "partial" f/partial partial
10+
[[+] [list 0]]
11+
[[] [42] [1 2 3 4]])
12+
13+
;; Clojurescript bug: https://dev.clojure.org/jira/browse/CLJS-3024
14+
#(:clj
15+
;; all (critical) arities of our IFn:
16+
(u/generate-tests "partial" f/partial partial
17+
[[list -2 -1]]
18+
[[0] [1]
19+
[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17]
20+
[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18]
21+
[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19]
22+
[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20]
23+
[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21]])))
24+
25+
(deftest constantly-test
26+
(u/generate-tests "constantly" f/constantly constantly
27+
[[1] [2]]
28+
[[] [4 5 6]]))
29+
30+
(deftest comp-test
31+
(u/generate-tests "comp" f/comp comp
32+
[[]]
33+
[[1]])
34+
(u/generate-tests "comp" f/comp comp
35+
[[-] [- *]]
36+
[[5 7] [1 3]])
37+
(u/generate-tests "comp" f/comp comp
38+
[[reverse reverse]]
39+
[[[]] [[1 2 3]]]))
40+
41+
(deftest complement-test
42+
(u/generate-tests "complement" f/complement complement
43+
[[nil?] [boolean]]
44+
[[nil] [42]]))
45+
46+
(deftest juxt-test
47+
(u/generate-tests "juxt" f/juxt juxt
48+
[[first count]]
49+
[["Hello"] [[1 2 3]]]))
50+
51+
(deftest fnil-test
52+
(u/generate-tests "fnil" f/fnil fnil
53+
[[list 42] [list 42 21]]
54+
[[nil 1] [nil 2 3] [1 2 3]]))
55+
56+
(deftest every-pred-test
57+
(u/generate-tests "every-pred" f/every-pred every-pred
58+
[[odd?] [even? #(> % 5)]]
59+
[[] [1] [1 2 3]]))
60+
61+
(deftest some-fn-test
62+
(u/generate-tests "some-fn" f/some-fn some-fn
63+
[[even? #(< % 10)]]
64+
[[] [1 2 3]]))
65+
66+
(deftest completing-test
67+
(u/generate-tests "completing" f/completing completing
68+
[[concat] [concat reverse]]
69+
[[[1]] [[1] [2]]]))
70+
71+
;; I don't think bound-fn* is doing much in clojurescript anyway
72+
73+
#?(:clj
74+
(deftest bound-fn*-test
75+
(def ^:dynamic *some-var* nil)
76+
(defn bound-fn*-test-f [res] (deliver res *some-var*))
77+
78+
(u/generate-tests "bound-fn*" f/bound-fn* bound-fn*
79+
[[list]]
80+
[])
81+
(let [res (promise)]
82+
(binding [*some-var* "goodbye"]
83+
(let [g (f/bound-fn* bound-fn*-test-f)]
84+
(.start (Thread. (fn []
85+
(g res))))))
86+
87+
(is (= (deref res 1000 :timeout)
88+
"goodbye")))))
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
(ns active.clojure.function-test-util)
2+
3+
(defmacro generate-tests [name repl orig fargs-list rargs-list]
4+
;; Note that 'is' must be emitted unhygienic, because we want to pickup cljs.test/is resp. clojure.test/is
5+
`(do
6+
~@(concat
7+
(map (fn [fargs]
8+
;; comparability is given:
9+
`(let [args# ~fargs]
10+
(~'is (= (apply ~repl args#)
11+
(apply ~repl args#))
12+
(str ~name " returns equal objects for equal arguments"))))
13+
fargs-list)
14+
(mapcat (fn [fargs]
15+
(map (fn [rargs]
16+
;; functionality same as original:
17+
`(~'is (= (apply (apply ~repl ~fargs) ~rargs)
18+
(apply (apply ~orig ~fargs) ~rargs))
19+
(str ~name " returns something that works the same as clojure.core equivalent")))
20+
rargs-list))
21+
fargs-list))))

test/active/clojure/test_runner.cljs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
[active.clojure.record-test]
99
[active.clojure.record-spec-test]
1010
[active.clojure.match-test]
11-
[active.clojure.config-test]))
11+
[active.clojure.config-test]
12+
[active.clojure.function-test]))
1213

1314
(doo-tests 'active.clojure.condition-test
1415
'active.clojure.debug-test
@@ -18,4 +19,5 @@
1819
'active.clojure.record-test
1920
'active.clojure.record-spec-test
2021
'active.clojure.match-test
21-
'active.clojure.config-test)
22+
'active.clojure.config-test
23+
'active.clojure.function-test)

0 commit comments

Comments
 (0)