-
Notifications
You must be signed in to change notification settings - Fork 372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add comp, constantly and complement #1179
Conversation
It looks like |
I agree that Clojure's order is the right one, since in mathematics, (g ∘ f)(x) usually means g(f(x)). |
Agree with gilch and Kodiologist, ((comp f g) x) means (f (g x)). Regarding tests, compose does obey three laws, provided function
This is how I test compose in hymn, even though it looks like overkill for such a simple function. :) |
heh, that's rather embarrassing. I'll fix that. Good that it was spotted so soon. Edit: actually, @pyx, can I just copy your implementation? It's concise and I don't think I can come up with anything like that. It's on a different license of course. |
@tuturto, sure, my pleasure, if you find that useful, please do whatever you want, including re-licensing, I didn't put a no-evil clause on it. :D |
@tuturto I think there are two sensible options:
(defn compose [f &rest fs]
"function composition"
(defn compose-2 [f g]
"compose 2 functions"
(fn [&rest args &kwargs kwargs]
(f (apply g args kwargs))))
(reduce compose-2 fs f)) should do |
Thank you @pyx. I added the atleast-one-argument version and fixed docs while I was at it. |
We should really follow Clojure's lead and choose option 1: return identity. |
I find it bit counter-intuitive that |
The idea is that the identity function is the identity element for the composition operator. That is, |
oh, hm.. If you put it that way it makes sense. |
May I suggest a minor improvement, (reduce compose-2 (list (rest fs)) (first fs)) can be (reduce compose-2 fs) |
Function calls are kind of expensive in Python I think it would be more efficient to use the imperative style, rather than calling a new function for every pair. Maybe something like this: def comp(*fs):
if not fs:
return identity
fs = reversed(fs)
def composed(*args,**kwargs):
for f in fs:
res = f(*args,**kwargs)
for f in fs:
res = f(res)
return res
return composed |
@gilch I think a benchmark or example showing an improvement should be a minimum requirement for a performance-motivated change. |
@Kodiologist, that's fair, but I'm starting to think Hy needs a timing macro. The Here's a Hy version: => (defn comp [&rest fs]
... (if (not fs) identity
... (= 1 (len fs)) (first fs)
... (do (setv fs (reversed fs))
... (fn [&rest args &kwargs kwargs]
... (for* [f fs] (setv res (apply f args kwargs))
... (for* [f fs] (setv res (f res))))
... res)))) |
(defn comp [&rest fs]
(if (not fs) identity
(= 1 (len fs)) (first fs)
(do (setv fs (reversed fs))
(fn [&rest args &kwargs kwargs]
(for* [f fs] (setv res (apply f args kwargs))
(for* [f fs] (setv res (f res)))) res))))
(import [time [time]])
(setv start (time))
(for [_ (range 100000)]
((comp inc inc inc inc) 1))
(print (- (time) start))
(print "iterative comp completed.")
(defn comp [&rest fs]
"function composition"
(defn compose-2 [f g]
"compose 2 functions"
(fn [&rest args &kwargs kwargs]
(f (apply g args kwargs))))
(if fs
(reduce compose-2 fs)
identity))
(setv start (time))
(for [_ (range 100000)]
((comp inc inc inc inc) 1))
(print (- (time) start))
(print "functional comp completed.")
|
@gilch But, then this composed function can only be called once, because the function list ( hy 0.11.0+353.gca6fd66 using CPython(default) 3.5.2 on Linux
=> (defn comp [&rest fs]
... (if (not fs) identity
... (= 1 (len fs)) (first fs)
... (do (setv fs (reversed fs))
... (fn [&rest args &kwargs kwargs]
... (for* [f fs] (setv res (apply f args kwargs))
... (for* [f fs] (setv res (f res)))) res))))
=> (def inc4 (comp inc inc inc inc))
=> (inc4 1)
5
=> (inc4 1)
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "<input>", line 263, in _hy_anon_fn_1
UnboundLocalError: local variable 'res' referenced before assignment
=> Here is my take: (defn comp [&rest fs]
(if (not fs) identity
(= 1 (len fs)) (first fs)
(do (setv rfs (reversed fs)
first-f (next rfs)
fs (tuple rfs))
(fn [&rest args &kwargs kwargs]
(setv res (apply first-f args kwargs))
(for* [f fs]
(setv res (f res)))
res)))) Notice:
|
Yeah, the nested
This has similar performance to @gilch's version. |
@Kodiologist hy 0.11.0+353.gca6fd66 using CPython(default) 3.5.2 on Linux
=> (defn comp [&rest fs]
... (if (not fs) identity
... (= 1 (len fs)) (first fs)
... (fn [&rest args &kwargs kwargs]
... (setv res (apply (get fs -1) args kwargs))
... (for [f (reversed fs)]
... (setv res (f res)))
... res)))
=> (def inc4 (comp inc inc inc (fn [n] (print "hit!") (inc n))))
=> (inc4 1)
hit!
hit!
6 Edit: spelling mistake |
Oh, right, whoops, I meant |
I think the most common case of compose, might be that the composed fuction will be re-used a lot, think And I wrote a naive time-it macro as mentioned by @gilch, I will open a new issue. |
Nice work and quite big difference in the speed already ❤️ |
Now that we have a proper (I hope so) timeit macro hylang/hyrule#39 (defn comp-f [&rest fs]
"function composition"
(defn compose-2 [f g]
"compose 2 functions"
(fn [&rest args &kwargs kwargs]
(f (apply g args kwargs))))
(if fs
(reduce compose-2 fs)
identity))
(defn comp-i1 [&rest fs]
(if (not fs) identity
(= 1 (len fs)) (first fs)
(fn [&rest args &kwargs kwargs]
(setv res (apply (get fs -1) args kwargs))
(for [f (cut fs -2 None -1)]
(setv res (f res)))
res)))
(defn comp-i2 [&rest fs]
(if (not fs) identity
(= 1 (len fs)) (first fs)
(do (setv rfs (reversed fs)
first-f (next rfs)
fs (tuple rfs))
(fn [&rest args &kwargs kwargs]
(setv res (apply first-f args kwargs))
(for* [f fs]
(setv res (f res)))
res)))) It's time to let the data speak: => (defn comp-f [&rest fs]
... "function composition"
... (defn compose-2 [f g]
... "compose 2 functions"
... (fn [&rest args &kwargs kwargs]
... (f (apply g args kwargs))))
... (if fs
... (reduce compose-2 fs)
... identity))
=> (defn comp-i1 [&rest fs]
... (if (not fs) identity
... (= 1 (len fs)) (first fs)
... (fn [&rest args &kwargs kwargs]
... (setv res (apply (get fs -1) args kwargs))
... (for [f (cut fs -2 None -1)]
... (setv res (f res)))
... res)))
=> (defn comp-i2 [&rest fs]
... (if (not fs) identity
... (= 1 (len fs)) (first fs)
... (do (setv rfs (reversed fs)
... first-f (next rfs)
... fs (tuple rfs))
... (fn [&rest args &kwargs kwargs]
... (setv res (apply first-f args kwargs))
... (for* [f fs]
... (setv res (f res)))
... res))))
=> (print ((comp-f inc inc inc inc) 1))
5
=> (print ((comp-i1 inc inc inc inc) 1))
5
=> (print ((comp-i2 inc inc inc inc) 1))
5
=> (time-it ((comp-f inc inc inc inc) 1))
11.021359261001635
=> (time-it ((comp-i1 inc inc inc inc) 1))
8.513512686997274
=> (time-it ((comp-i2 inc inc inc inc) 1))
12.34987382899999
=> (time-it (inc4 1) (setv inc4 (comp-f inc inc inc inc)))
4.459002538002096
=> (time-it (inc4 1) (setv inc4 (comp-i1 inc inc inc inc)))
4.978545720001421
=> (time-it (inc4 1) (setv inc4 (comp-i2 inc inc inc inc)))
3.3503086150012678 Calling => (time-it (inc10 1) (setv inc10 (comp-f inc inc inc inc inc inc inc inc inc inc)))
11.87790252600098
=> (time-it (inc10 1) (setv inc10 (comp-i1 inc inc inc inc inc inc inc inc inc inc)))
7.043803205000586
=> (time-it (inc10 1) (setv inc10 (comp-i2 inc inc inc inc inc inc inc inc inc inc)))
5.294795802998124 |
Now that I think pipe - the counterpart of comp (defn pipe [&rest fs]
"docstring goes here"
(apply comp (reversed fs))) in use => (def inc-str (pipe int inc str))
=> (inc-str "41")
'42' juxt - I saw this one when I read about the "see also" part on clojure-docs, did not read the implementation though (defn juxt [f &rest fs]
"docstring goes here"
(setv fs (cons f fs))
(fn [&rest args &kwargs kwargs]
(setv res [])
(for* [f fs]
(.append res (apply f args kwargs)))
res)) I can imagine => ((juxt min max sum) (range 1 101))
[1, 100, 5050]
=> (setv [total difference product quotient] ((juxt + - * /) 24 3))
[27, 21, 72, 8.0]
=> (import [operator [itemgetter]])
=> ((juxt (itemgetter 'name) (itemgetter 'age)) {'name 'Joe 'age 42 'height 182})
['Joe', 42] |
@pyx, this PR is for only the three functions named in the title. Those should be in their own PR if you want them. |
@pyx you could make a new issue first for those. From the timing data we see that the You could use |
@Kodiologist, fair enough, but usually
|
@gilch Since I am new here, sometimes I don't know which way should I go, e.g, I am not sure if creating a new issue regarding yet-to-be merged PR is a proper way of giving feedback. That's why I did not draft a PR myself in the first place, I know it should come with proper documentation and test cases, to which the requirements I am not quite sure about. I will try, for About |
I have [1] That's why I proposed |
@pyx Wait until this one is merged. Not because of a merge conflict, but because |
@gilch I sort of regret even bringing up the subject of benchmarks because any discussion about performance is speculation outside of profiling a real program. Premature optimization is the root of all evil. Anyway, in my opinion, any of the three versions is fine for now. Do you approve this PR? |
@Kodiologist , okay, I will add |
We don't know either. Don't worry too much about it, you're doing fine. I would approve adding Merge conflicts in this file could probably be avoided if the exports list had only one element per line. The merge conflicts in this case are trivial to resolve though.
I normally agree with that maxim, but I feel you're using it out of context. When developing an application, it's unlikely that any given function will be on the critical path, so it's best to profile and see what's actually the bottleneck. Optimizing other parts is just a waste of time and the optimized code can be harder to maintain too. However, when developing the standard library, we're not just making one application, but the building blocks of every application written in Hy. Therefore it's much more likely the functions we write will be on the critical path of some Hy applications, especially for something as fundamental as function composition! So I do feel it's worth some effort not to be too inefficient here. That said, proving optimality is probably an undecidable problem in the general case, so we do have to give up eventually. But you do hit diminishing returns pretty quickly in practice for a function this short.
|
I'm not really seeing a use case for |
@kirbyfan64 Good thinking; |
@gilch this is my thought on file -> file We can build the pipeline like this: (def process
(pipe
read-file
parse-file
transform-phase-1
transform-phase-2
transform-phase-3
rename
write-file)) It's just more natural to read in this case than If we can have both |
@kirbyfan64 @gilch , I totally agree technically hy 0.11.0+357.g29b3702 using CPython(default) 3.5.2 on Linux
=> (def my-even? (comp not odd?))
Traceback (most recent call last):
File "<input>", line 1, in <module>
NameError: name 'not' is not defined
=> not
Traceback (most recent call last):
File "<input>", line 1, in <module>
NameError: name 'not' is not defined
=> (not True)
False If I remember correctly, you have an issue or milestone regarding this and BTW, happy holidays, everyone. |
Yes, #1103. I'm guessing it will be pretty straightforward. |
PS. my |
I wonder... |
I updated |
relates #1176