Clojure

-> and ->> have unexpected behavior when combined with unusual macros

Details

  • Type: Defect Defect
  • Status: Closed Closed
  • Priority: Minor Minor
  • Resolution: Completed
  • Affects Version/s: Release 1.5
  • Fix Version/s: Release 1.6
  • Component/s: None
  • Labels:
    None
  • Patch:
    Code and Test
  • Approval:
    Ok

Description

My intuitive understanding of the classic threading macros is that the meaning of forms like (-> a b c) can be understood syntactically independent of the meaning of the symbols involved or the fact that the two threading macros are defined recursively. However the recursive definition breaks that expectation. After

(macroexpand-1 (macroexpand-1 '(-> a b c)))

=> (c (-> a b))

c is now in control if it is a macro, and is now seeing the argument (-> a b) rather than (b a) as would be the case if we had written (c (b a)) originally.

Admittedly I do not know of a realistic example where this is an important distinction (I noticed this when playing with a rather perverse use of ->> with macros from korma), but at the very least it means that the behavior of the threading macros isn't quite as easy to accurately explain as I thought it was.

Activity

Gary Fredericks made changes -
Field Original Value New Value
Patch Code and Test [ 10002 ]
Attachment 0001-CLJ-1121-Reimplement-and-without-recursion.patch [ 11751 ]
Hide
Gary Fredericks added a comment -

I just realized that my patch also implements CLJ-1086.

Show
Gary Fredericks added a comment - I just realized that my patch also implements CLJ-1086.
Gary Fredericks made changes -
Description My intuitive understanding of the classic threading macros is that the meaning of e.g. {{(-> a b c)}} can be understood syntactically independent of the meaning of the symbols involved or the fact that {{->}} and {{->>}} are defined recursively. However the recursive definition breaks that expectation. Two expansions on {{(-> a b c)}} yields {{(c (-> a b))}}, where {{c}} is now in control if it is a macro, and is now seeing the argument {{(-> a b)}} rather than {{(b a)}} as would be the case if we had written {{(c (b a))}} originally.

Admittedly I do not know of a realistic example (I noticed this when playing with a rather perverse use of {{->>}} with macros from korma), but at the very least it means that the behavior of the threading macros isn't quite as easy to accurately explain as I thought it was.
My intuitive understanding of the classic threading macros is that the meaning of e.g. {{(-> a b c)}} can be understood syntactically independent of the meaning of the symbols involved or the fact that {{->}} and {{->>}} are defined recursively. However the recursive definition breaks that expectation. Two expansions on {{(-> a b c)}} yields {{(c (-> a b))}}, where {{c}} is now in control if it is a macro, and is now seeing the argument {{(-> a b)}} rather than {{(b a)}} as would be the case if we had written {{(c (b a))}} originally.

Admittedly I do not know of a realistic example where this is an important distinction (I noticed this when playing with a rather perverse use of {{->>}} with macros from korma), but at the very least it means that the behavior of the threading macros isn't quite as easy to accurately explain as I thought it was.
Gary Fredericks made changes -
Description My intuitive understanding of the classic threading macros is that the meaning of e.g. {{(-> a b c)}} can be understood syntactically independent of the meaning of the symbols involved or the fact that {{->}} and {{->>}} are defined recursively. However the recursive definition breaks that expectation. Two expansions on {{(-> a b c)}} yields {{(c (-> a b))}}, where {{c}} is now in control if it is a macro, and is now seeing the argument {{(-> a b)}} rather than {{(b a)}} as would be the case if we had written {{(c (b a))}} originally.

Admittedly I do not know of a realistic example where this is an important distinction (I noticed this when playing with a rather perverse use of {{->>}} with macros from korma), but at the very least it means that the behavior of the threading macros isn't quite as easy to accurately explain as I thought it was.
My intuitive understanding of the classic threading macros is that the meaning of forms like {{(-> a b c)}} can be understood syntactically independent of the meaning of the symbols involved or the fact that the two threading macros are defined recursively. However the recursive definition breaks that expectation. After

{noformat}
(macroexpand-1 (macroexpand-1 '(-> a b c)))

=> (c (-> a b))
{noformat}

{{c}} is now in control if it is a macro, and is now seeing the argument {{(-> a b)}} rather than {{(b a)}} as would be the case if we had written {{(c (b a))}} originally.

Admittedly I do not know of a realistic example where this is an important distinction (I noticed this when playing with a rather perverse use of {{->>}} with macros from korma), but at the very least it means that the behavior of the threading macros isn't quite as easy to accurately explain as I thought it was.
Hide
Stuart Halloway added a comment -

Would be nice if tests also demonstrated that metadata is preserved correctly.

Show
Stuart Halloway added a comment - Would be nice if tests also demonstrated that metadata is preserved correctly.
Hide
Gary Fredericks added a comment -

New patch in response to stuarthalloway feedback.

Show
Gary Fredericks added a comment - New patch in response to stuarthalloway feedback.
Gary Fredericks made changes -
Attachment CLJ-1121-v002.patch [ 11780 ]
Hide
Tim McCormack added a comment - - edited

This patch also prevents an infinite loop in the macroexpander when fed the following expression:

(->> a b (->> c d))

Edit: Far simpler example.

Show
Tim McCormack added a comment - - edited This patch also prevents an infinite loop in the macroexpander when fed the following expression:
(->> a b (->> c d))
Edit: Far simpler example.
Stuart Halloway made changes -
Approval Triaged [ 10120 ]
Rich Hickey made changes -
Approval Triaged [ 10120 ] Vetted [ 10003 ]
Rich Hickey made changes -
Fix Version/s Release 1.6 [ 10157 ]
Stuart Halloway made changes -
Assignee Stuart Halloway [ stu ]
Stuart Halloway made changes -
Approval Vetted [ 10003 ] Screened [ 10004 ]
Rich Hickey made changes -
Approval Screened [ 10004 ] Ok [ 10007 ]
Stuart Halloway made changes -
Resolution Completed [ 1 ]
Status Open [ 1 ] Closed [ 6 ]

People

Vote (4)
Watch (5)

Dates

  • Created:
    Updated:
    Resolved: