Closed timsgardner closed 9 years ago
Just thought of an (admittedly weird) case where hoisting a closureless anonoymous fn makes a semantic difference:
(def holder (atom nil))
(defn test-hoisting-assumption []
(let [state (atom nil)
log (atom [])]
(let [f (fn bla []
(if (not= @holder bla)
(do (reset! holder bla)
false)
true))]
(dotimes [_ 2]
(reset! state (f))))
(swap! log conj @state)
(reset! state nil)
(dotimes [_ 2]
(reset! state
((fn bla []
(if (not= @holder bla)
(do (reset! holder bla)
false)
true)))))
(swap! log conj @state)))
Then:
anonohoist=> (test-hoisting-assumption)
[true false]
Maybe named self-reference counts as a closure? Are there any cases in which a closureless anonymous function without named self-reference cannot be hoisted?
(defn test-hoisting-assumption-2 []
[(loop [f nil, i 3]
(let [f2 (fn [x] x)]
(if (< 0 i)
(recur f2 (dec i))
(= f f2))))
(let [f2 (fn [x] x)]
(loop [f nil, i 3]
(if (< 0 i)
(recur f2 (dec i))
(= f f2))))])
anonohoist=> (test-hoisting-assumption-2)
[false true]
how do programming languages even work you guys
What about a case in which a closureless anonymous function cannot be hoisted to just under the closest recursion point - loop or fn or whatever? And maybe we should try to prove the hoisting thing rather than just looking for counterexamples off the top of our heads
If you hoist to just under the nearest recursion point you aren't going to pop the function out of an inner loop anyway, so it would be completely pointless. So long as you have first-class functions with reference equality semantics (unlike, say, Mathematica, where functions have lexical equality semantics, or whatever you want to call it), you can't safely hoist them while preserving the semantics of not hoisting them. Closing this issue as stupid-idea-in-the-first-place.
No, I'm not convinced this is a dumb idea.
Paying GC/allocation cost for using a pure function is kind of insane. Really, there should be one instance of the function class at all and you just use that. A singleton, to use OO design pattern parlance.
15x penalty is pretty bad. Requiring users to manually optimize using let bindings is also pretty bad.
It isn't just a perf thing. How would hoisting work logically so long as functions have reference semantics? (not= (fn [x] x) (fn [x] x))
This works though:
(defn test-hoisting-assumption-3 []
[(loop [f nil, i 3]
(let [f2 (fn [x] x)]
(if (< 0 i)
(recur f2 (dec i))
(= (class f) (class f2)))))
(let [f2 (fn [x] x)]
(loop [f nil, i 3]
(if (< 0 i)
(recur f2 (dec i))
(= (class f) (class f2)))))])
anonohoist=> (test-hoisting-assumption-3)
[true true]
Do you mean that hoisting would break the equality semantics of fns?
(= (fn [x] x) (fn [x] x))
;; false
(defn foo [x]
:bar)
(= foo foo)
;; true
(= #'foo #'foo)
;; true
I could see the first expression returning true if we hoist/do a singleton thing
Your example works because each fn form in the source gets its own class, so everything is referring to that one class. Mine returns false because there are two fn forms.
Yes, it breaks the equality semantics of fns, which is fundamental to the meaning of the language. Mathematica gets away with lexical equality because it's a symbolic system with anonymous functions represented as transparent rewritable expressions. fn forms in Clojure are like reify forms, every time you hit one you're supposed to construct something (it now seems to me) and things get weird if you don't.
:(
that is my formal response
So anonymous functions should never be used in an inner loop, then? That kind of sucks.
For example, if you don't construct something new at every fn form you have the equality semantics oscillating wildly depending on whether the fn form closes over something, which is nutso
Right right right
Yeah never use anonymous functions in an inner loop I guess
worst christmas ever
I hope all our repo watchers are appreciating the nuances of this chat-in-an-issue
Always Remember: Lambdas Are For Losers, Lisp Is For Lame-Ohs
KTC needs a t-shirt printer for gems like that
Closures aren't free. You'll encounter the same trouble pretty much everywhere (i.e. JavaScript etc.). Some languages like Java 8 do optimize these cases but the win is going to small unless it works for pipelines at which point you might as well be using transducers (Java 8 does a transducer like transform).
That makes sense. It sucks to have to pay the perf cost even when the anonymous function isn't closing over anything, though.
Presumably if an anonymous function has no closures it can be hoisted to the top level, wherever that is, and constructed just once, rather than however many times it might be in an inner loop (or whatever). This is not happening.
With the above AOT'd:
~ 15 times slower.
Disassembled
invoke
for test-hoisting:The anonymous function is constructed 100,000 times in the first case, once in the second. (Bit weird that that only slows us down by 10x.) Also, even in the second case we seem to be unnecessarily casting the fn
obj
toIFn
100,000 times, not sure what the perf implications of that are.