Proposed change to let-> syntax

718 views
Skip to first unread message

Alex Nixon

unread,
Nov 15, 2012, 1:35:24 PM11/15/12
to clo...@googlegroups.com
Hi all,

I find the proposed function let-> in Clojure 1.5 very useful, but a bit ugly.  The arguments are backwards when compared to vanilla let, and it doesn't support destructuring where it easily could (which I believe would be helpful when threading 'state-like' maps, as I find let-> very useful for).

I'd like to float an alternative implementation which improves on both these issues.  Thoughts?

(defmacro new-let->
  "Establishes bindings as provided for let, evaluates the first form
   in the lexical context of that binding, then re-establishes bindings
   to that result, repeating for each successive form"
  [bindings & forms]
  (assert (vector? bindings) "binding must be a vector")
  (assert (= 2 (count bindings)) "binding vector must contain exactly two forms")
  `(let [~@bindings
         ~@(interleave (repeat (bindings 0)) (drop-last forms))]
     ~(last forms)))

(new-let-> [{:keys [foo bar] :as state} {:foo 1 :bar 2}]
  (assoc state :foo (inc bar))
  (assoc state :bar (inc foo))) ; => {:foo 3, :bar 4}

-- 
Alex Nixon

Software Engineer | SwiftKey

al...@swiftkey.net | http://www.swiftkey.net/

++++++
WINNER - MOST INNOVATIVE MOBILE APP - GSMA GLOBAL MOBILE AWARDS 2012

Head office: 91-95 Southwark Bridge Road, London, SE1 0AX TouchType is a limited company registered in England and Wales, number 06671487. Registered office: 91-95 Southwark Bridge Road, London, SE1 0AX

Mark Engelberg

unread,
Nov 15, 2012, 5:41:56 PM11/15/12
to clo...@googlegroups.com
Where did you find the proposal?  I can't find any info about let->

Andy Fingerhut

unread,
Nov 15, 2012, 5:44:34 PM11/15/12
to clo...@googlegroups.com, clo...@googlegroups.com
Check git commit logs from a month or so ago.  Rich Hickey committed it.

Andy

Sent from my iPhone

On Nov 15, 2012, at 2:41 PM, Mark Engelberg <mark.en...@gmail.com> wrote:

Where did you find the proposal?  I can't find any info about let->

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en

Alex Nixon

unread,
Nov 15, 2012, 6:21:44 PM11/15/12
to clo...@googlegroups.com
For reference, the current implementation.

Mark Engelberg

unread,
Nov 15, 2012, 6:34:11 PM11/15/12
to clo...@googlegroups.com
Thanks Alex.

My two cents is that your syntax is more the way I'd expect it to look, starting off like an actual let with support for destructuring, followed by the forms.

Only advantage I can think of for the one in core is that it enforces that you only have one binding.  Basing let-> off of let notation might cause people to think you could bind multiple vars to values in the initial bracketed part; that would be somewhat nonsensical. 

Alex Nixon

unread,
Nov 15, 2012, 6:56:21 PM11/15/12
to clo...@googlegroups.com
Thanks for the comments.

Regarding the bindings, I'd point out that if-let and when-let already work this way (enforcing one binding) and so it isn't introducing any inconsistency.

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en

Alan Malloy

unread,
Nov 15, 2012, 8:17:42 PM11/15/12
to clo...@googlegroups.com
The primary point of let-> is that you can insert it into an existing -> pipeline. 

(-> foo
    (stuff)
    (blah)
    (let-> foo-with-stuff
      (for [x foo-with-stuff]
        (inc x)))

Your proposal breaks this.

Mark Engelberg

unread,
Nov 15, 2012, 8:25:47 PM11/15/12
to clo...@googlegroups.com
On Thu, Nov 15, 2012 at 5:17 PM, Alan Malloy <al...@malloys.org> wrote:
The primary point of let-> is that you can insert it into an existing -> pipeline.


That makes sense.

Alex Nixon

unread,
Nov 16, 2012, 7:32:54 AM11/16/12
to clo...@googlegroups.com
It does - thanks for the clarification.

So is let-> intended to be *never* used outside of ->? If so, can an argument be made for enforcing its use within -> to avoid (as far as I'm aware) introducing a 'new' (value first, name second, no destructuring support) binding syntax into core?  Despite it being more verbose, I'd rather read (-> 42 (let-> meaning-of-life (inc))) than (let-> 42 meaning-of-life (inc)).

And on destructuring - the closest I can get with keeping compatibility with existing -> forms would be

(-> {:foo 1}
  (let-> {:keys [foo] :as x}
    (assoc x :bar :foo)))

The pro is that you get the power of destructuring.  The con is that this would be the first occurrence of destructuring from outside of an explicit binding form.

Jay Fields

unread,
Nov 16, 2012, 9:44:11 AM11/16/12
to clo...@googlegroups.com
I think using 'let' is what makes this confusing.

I'd like to have a macro/fn for both ideas being discussed in this
thread, ideally they'd both be named in a way that causes the least
amount of confusion.

I'm not sure what those names are, perhaps

(-> 1 (inc) (rebind a-num
(- 2 a-num)
(dec a-num))
=> -1

(let-rebind [{:keys [a b] :as state} {:a 1 :b 2 :c 9}]
(assoc state :a (inc a))
(assoc state :b (inc b)))
=> {:a 2 :b 3 :c 9}

Jay Fields

unread,
Nov 16, 2012, 9:46:59 AM11/16/12
to clo...@googlegroups.com
another thought - a really nice thing about if, let, and if-let is
that if you know how to use if and let, if-let just makes sense. You
can't say the same about ->, let, and let-> with the current proposal.

On Fri, Nov 16, 2012 at 7:32 AM, Alex Nixon <al...@swiftkey.net> wrote:

László Török

unread,
Nov 16, 2012, 10:49:43 AM11/16/12
to clo...@googlegroups.com
+1, looking at the latest master, I think they need a better docstring, or rather an example of use that makes it easier to grasp.

Regards,

Laszlo

2012/11/16 Jay Fields <j...@jayfields.com>



--
László Török

Rich Hickey

unread,
Nov 30, 2012, 10:37:44 AM11/30/12
to clo...@googlegroups.com
I'm not satisfied with the names for the new threading macros either.

The new names being considered for let->, test-> and when-> are:

A) let-> becomes as->

reduces arg order and destructuring expectations.

B) test-> becomes cond->

cond-> was the original name, and, protestations about not short-circuiting like cond notwithstanding, it is still the best fit. The doc string will say:

"Note that, unlike cond branching, cond-> threading does
not short circuit after the first true test expression."

C) when-> becomes some->

and in doing so, tests for non-nil rather than truth.

==========
This last one touches on the general area of non-nil-ness, often needed. Other possible (future) ideas in line with this are:

(some? x) == (not (nil? x))

(some coll) ;;note no pred, == (some some? coll)

this is tricky, as we are bridging CL's some and Maybe's Some

if-some, when-some et al.

(some! x) ;;(or somex ?) identity for all but nil, which throws

etc
==========

Last chance for fabulous and better names than these: (as->, cond-> and some->). Note that "cond-> doesn't match cond's short-circuit" has been considered already, please do not repeat.

Thanks,

Rich

Jay Fields

unread,
Nov 30, 2012, 12:33:12 PM11/30/12
to clo...@googlegroups.com
On Fri, Nov 30, 2012 at 10:37 AM, Rich Hickey <richh...@gmail.com> wrote:
> The new names being considered for let->, test-> and when-> are:
>
> A) let-> becomes as->

I prefer ->as, but don't feel strongly about it.

(-> 1
str
(->as one-str
(count one-str)
(* 2 one-str))) ;; returns 2

The thing that I like about this is that it reads to me as "thread
as", and the ordering of "->" and "as" reminds me that the args are
slightly different from traditional threading macros (i.e. it's not
(thread-macro x & forms).

> B) test-> becomes cond->

I don't like cond-> for the same reason I didn't like let->, the
cond-> isn't easy to understand if you already understand cond and ->.
To try to come up with a name that I like, I tried to write some
similar code to see what I would use. I came up with the following:

(->> 1
(#(if (number? %) (str %) %))
(#(if (string? %) (count %) %))) ;; returns 1

Which led me to the following (the best I can come up with right now).

(if-> 1
number? str
string? count) ;; returns 1

I don't think this is a homerun, as the understanding of "if" and "->"
wouldn't guarantee an understanding of this. However, I couldn't come
up with an example of solving a similar problem with -> and cond in
any sensible way. I don't think the shape of the arguments should
define the name, which is the only reason you'd want to use "cond",
imho.

I also considered something that expressed "if or identity. Perhaps
something like the following

(ifoi-> 1
number? str
string? count) ;; ifoi stands for "if or identity".

You could also use ifei->, for "if else identity"

Perhaps the issue is resolved if the form can take an else fn. Then
the original if-> example becomes

(if-> 1
number? str identity
string? count identity)

This also gives you the freedom to put any function you want in the
else clause. I don't know if that's a good thing or not.

Or, since the significant majority of us have auto-complete, perhaps
the following works

(if-else-identity-> 1
number? str
string? count)

> C) when-> becomes some->

I don't see any issue with when->, that one actually seems very clear.
Is the issue that you want to be able to continue the threading with
false? If so, I wouldn't overload the meaning of "some", I'd simply
make it take a test fn - then you can use any pred you want.

(->when (comp not nil) (f1) (f2))

Which is even more clear if you define not-nil? in core. Then all of
the following would easily work

(->when 1 not-nil? (f1) (f2)) ; thread when not nil
(->when 1 identity (f1) (f2)) ; thread when truthy
(->when 1 #{1 2 3} (f1) (f2)) ; thread when the value is 1 2 or 3.

note, I switched the order again, as I like the way it reads (e.g.
"thread when 1 is not nil"), and it reminds me that this is a
threading macro that takes more than x & forms.

Alex Baranosky

unread,
Nov 30, 2012, 1:15:39 PM11/30/12
to clo...@googlegroups.com
I've got a utility function I've been using called `conditionally-transform` which is a non-macro version of `test->`.  I think both cond-> and if-> have a similar problem in that, if you already understand if/cond/-> then it gives you little insight into how the new threading macro works. 

Both these names focus on the conditional nature of the macro.  Perhaps a name based on the transformative nature of the macro: trans-> or transform-> ?  These convey the idea of threading through various transformations.

For when->, how about non-nil-> ?  I'd feel better about some-> if it came, as you said, with other new functions that use the syllable 'some', thus expanding the Clojure lexicon.  As it stands I don't feel like some-> has a particularly intuitive meaning.

Alex

Steve Miner

unread,
Nov 30, 2012, 1:49:34 PM11/30/12
to clo...@googlegroups.com
I propose guard-> to avoid the cond-> confusion.

If we're voting, as-> is good. I liked when->.

Ben Wolfson

unread,
Nov 30, 2012, 2:23:59 PM11/30/12
to clo...@googlegroups.com
On Fri, Nov 30, 2012 at 10:15 AM, Alex Baranosky
<alexander...@gmail.com> wrote:
> I've got a utility function I've been using called `conditionally-transform`
> which is a non-macro version of `test->`.

Likewise, except with use a HOF called "conditionalize":

(defn conditionalize
"When called with a function f, returns a function that returns its
arguments if they
arguments do not satisfy pred, or the result of the application of
f to its arguments otherwise."
[pred f]
(fn [& args] (if (apply pred args) (apply f args) (apply identity args))))

Jay's example would then be (-> x ((conditionalize number? str))
(conditionalize string? count))), which tbh I like more because (a) it
doesn't introduce a new macro that can only be used in one syntactic
position (and there are other cases where you'd want to be able to
pass these transformed functions around) and (b) it groups the
transformations syntactically with their tests.

--
Ben Wolfson
"Human kind has used its intelligence to vary the flavour of drinks,
which may be sweet, aromatic, fermented or spirit-based. ... Family
and social life also offer numerous other occasions to consume drinks
for pleasure." [Larousse, "Drink" entry]

Ben Wolfson

unread,
Nov 30, 2012, 2:24:15 PM11/30/12
to clo...@googlegroups.com
On Fri, Nov 30, 2012 at 10:15 AM, Alex Baranosky
<alexander...@gmail.com> wrote:
> I've got a utility function I've been using called `conditionally-transform`
> which is a non-macro version of `test->`.

Likewise, except with use a HOF:

(defn conditionalize [pred f]
(fn [& args] (if (apply pred args) (apply f args) (apply identity args))))

Jay's example would then be (-> x ((conditionalize number? str))
(conditionalize string? count))).

Rich Hickey

unread,
Nov 30, 2012, 2:49:49 PM11/30/12
to clo...@googlegroups.com

On Nov 30, 2012, at 1:49 PM, Steve Miner wrote:

> I propose guard-> to avoid the cond-> confusion.
>

Yeah, that came up. Guards in other langs are short circuiting, just like cond.

Another in that camp was gate->

Rich Hickey

unread,
Nov 30, 2012, 2:47:56 PM11/30/12
to clo...@googlegroups.com

On Nov 30, 2012, at 1:15 PM, Alex Baranosky wrote:

> I've got a utility function I've been using called `conditionally-transform` which is a non-macro version of `test->`. I think both cond-> and if-> have a similar problem in that, if you already understand if/cond/-> then it gives you little insight into how the new threading macro works.
>
> Both these names focus on the conditional nature of the macro. Perhaps a name based on the transformative nature of the macro: trans-> or transform-> ? These convey the idea of threading through various transformations.
>

All threading macros are transforms. cond-> has test forms like cond.

> For when->, how about non-nil-> ? I'd feel better about some-> if it came, as you said, with other new functions that use the syllable 'some', thus expanding the Clojure lexicon. As it stands I don't feel like some-> has a particularly intuitive meaning.


Intuitive meaning is not always available, given everyone has different intuitions :)

Sean Corfield

unread,
Nov 30, 2012, 4:53:45 PM11/30/12
to clo...@googlegroups.com
On Fri, Nov 30, 2012 at 7:37 AM, Rich Hickey <richh...@gmail.com> wrote:
A) let-> becomes as->

Fine with that.
 
B) test-> becomes cond->

Fine with that (because I can't think of anything better).
 
C) when-> becomes some->

and in doing so, tests for non-nil rather than truth.

Given that some-> threads while non-nil but the fn some stops with the first logical true value, this seems counter-intuitive to me. when-> seems better here, or while-> perhaps? What other names were considered?
--
Sean A Corfield -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
World Singles, LLC. -- http://worldsingles.com/

"Perfection is the enemy of the good."
-- Gustave Flaubert, French realist novelist (1821-1880)

Alex Baranosky

unread,
Nov 30, 2012, 6:32:51 PM11/30/12
to clo...@googlegroups.com
gate-> is an interesting possiblity.

--

Steve Miner

unread,
Dec 1, 2012, 9:31:08 AM12/1/12
to clo...@googlegroups.com
gate-> would work. Like guard-> it doesn't have any connotations in the Clojure world, but it's learnable. I'll add one more: qual-> ... short for "qualified threading macro". Each clause is qualified by a test condition.

Of course, there's always conde-> to borrow from miniKanren and core.logic. The "e" stands for "every" because multiple clauses can succeed as opposed to the short-circuiting cond.

Rich Hickey

unread,
Dec 1, 2012, 9:33:57 AM12/1/12
to clo...@googlegroups.com

On Nov 30, 2012, at 4:53 PM, Sean Corfield wrote:

> On Fri, Nov 30, 2012 at 7:37 AM, Rich Hickey <richh...@gmail.com> wrote:
> A) let-> becomes as->
>
> Fine with that.
>
> B) test-> becomes cond->
>
> Fine with that (because I can't think of anything better).
>
> C) when-> becomes some->
>
> and in doing so, tests for non-nil rather than truth.
>
> Given that some-> threads while non-nil but the fn some stops with the first logical true value, this seems counter-intuitive to me. when-> seems better here, or while-> perhaps? What other names were considered?

when and while also use logical truth. We don't have anything where the basis is non-nil. Thus my extended description of how some* could come to occupy that role.

Avoiding some-> due to the less-often used function means the only other viable alternatives are:

a completely new name
thread only logical truth (i.e. can't thread false)

one alternative was:

is->


Rich Hickey

unread,
Dec 1, 2012, 9:48:12 AM12/1/12
to clo...@googlegroups.com
I'll argue that if 'e' in conde is enough to imply 'each' then '->' in cond-> is enough to imply it keeps threading.

I think many people have ideas about -> operators born of some of these libraries that supply a wealth of 'things you can use in ->'. Most of their operators have '->' in their names, but don't fundamentally thread - e.g. they are terminators or one shots like if-> (or ->if).

A op-> operator, IMO, should take an open set of expressions and thread the return values through them in some way. Otherwise it shouldn't be an op->.

When one reads -> as 'thread' vs 'for use in threading', things might become clearer.

Andy Fingerhut

unread,
Dec 1, 2012, 12:38:54 PM12/1/12
to clo...@googlegroups.com
I don't have suggestions for the best names for these new things, but one good example in the doc string would go a long way to making it clearer what they *do* and how they are intended to be used. The doc strings are written once, but read thousands of times, and are your most reliable line of communication to Clojure developers. Everyone can find them quickly.

Andy

Marek Šrank

unread,
Dec 1, 2012, 1:33:31 PM12/1/12
to clo...@googlegroups.com
+1

Sean Corfield

unread,
Dec 1, 2012, 2:12:04 PM12/1/12
to clo...@googlegroups.com
On Sat, Dec 1, 2012 at 6:33 AM, Rich Hickey <richh...@gmail.com> wrote:
> Given that some-> threads while non-nil but the fn some stops with the first logical true value, this seems counter-intuitive to me. when-> seems better here, or while-> perhaps? What other names were considered?
when and while also use logical truth. We don't have anything where the basis is non-nil. Thus my extended description of how some* could come to occupy that role.

Ah, OK, so the change in semantics is specifically to allow threading of true/false (as well as other non-nil values)... your intent is much clearer to me now, thanx. I'm fine with some-> after hearing that.
-- 

Alex Baranosky

unread,
Dec 1, 2012, 3:08:21 PM12/1/12
to clo...@googlegroups.com
The more I watch this conversation, the more I like some-> and cond->.  What was the motivation for changing let-> to as-> ? let-> made a lot of sense as a name to me.

Alex Baranosky

unread,
Dec 1, 2012, 3:10:01 PM12/1/12
to clo...@googlegroups.com
Nevermind that.  The answer is that as-> would "reduce arg order and destructuring expectations".  Makes sense.  So for whatever small amount it is worth I officially have been convinced of all the new names.

Terje Norderhaug

unread,
Dec 2, 2012, 8:30:53 PM12/2/12
to clo...@googlegroups.com
On Thu, Nov 15, 2012 at 5:17 PM, Alan Malloy <al...@malloys.org> wrote:
> The primary point of let-> is that you can insert it into an existing ->
> pipeline.
>
> (-> foo
> (stuff)
> (blah)
> (let-> foo-with-stuff
> (for [x foo-with-stuff]
> (inc x)))

This use case of the macro now renamed as-> in Clojure 1.5 is already
covered by using fn in a pipeline:

(-> foo
(stuff)
(blah)
((fn [foo-with-stuff]
(for [x foo-with-stuff]
(inc x)))))

A benefit of using fn here is that it also works with ->> just the
same. However, there is a place for a construct to simplify multiple
uses of fn in a pipeline like in:

(-> foo
(stuff)
(blah)
((fn [foo]
(for [x foo]
(inc x))))
((fn [foo]
(for [x foo]
(dec x)))))

I propose fn-> as name for a construct similar to as-> used like this:

(-> foo
(stuff)
(blah)
((fn-> [foo]
(for [x foo]
(inc x))
(for [x foo]
(dec x)))))

The fn-> macro defines an anonymous function using the same syntax as
fn, with its forms threaded like in as-> where each evaluated form is
bound to a symbol.

In the example above, the result of the first 'for' form is bound to
the 'foo' used by the second 'for' form. That is, within fn-> the
items in the list bound to foo is first incremented and the resulting
list bound to foo, then the items in the list bound to foo is
decremented with the resulting list returned (incidentally leaving the
list as before).

Keeping the syntax and semantics of fn-> similar to fn provides an
smooth upgrade path from current uses of fn in pipelines.

-- Terje Norderhaug

Terje Norderhaug

unread,
Dec 2, 2012, 8:57:01 PM12/2/12
to clo...@googlegroups.com
Here is the short form of the proposal below:

1. Make as-> in Clojure 1.5 have a syntax and semantic matching how fn
is used in pipelines.
2. Rename as-> to fn-> reflecting its similarity to fn.

-- Terje Norderhaug

Daniel Dinnyes

unread,
Jul 19, 2013, 4:24:13 AM7/19/13
to clo...@googlegroups.com
Hi,

This will sound strange from me, but are these macros really to be added to the core?

Firstly, I don't see they extend the language in any way new. Instead they ease some very special cases, which could have been done with the combination of existing core functions/macros just as well, and that even might be more readable (as the confusion around naming and meaning suggests). It is a complexity. I would expect that everything in the core is important to know. If it turns out these new macros will not be used that much in real life (it doesn't matter if there are real life code where it COULD be used, what matters is that eventually will it GET used there), then it's just a burden for comprehending the core for any newcomer.

Second, I have not seen myself using similar macros from other "contrib-like" libraries, neither writing similar stuff myself. I often feel the limitations of the parameter order of the "->" and "->>" macros, yes, but until we get feedback on the usage of such new macros, that they really get traction, I would feel worried about including them in the core name-space (for example in any reasonable project I find myself overriding "group-by" with a three parameter version).

And thirdly, the "why" to my last sentence: because including them in the core namespace is a commitment. It should not be just removed later if it turns out no one really needs them. Should not be modified, or patched up, or prettified, or renamed later either. Because it can break someone's production code, someone's business, maybe someone's L.I.F.E. I'm not saying it CAN'T be changed, but it can reflect bad on us. There is no problem in saying "well, it turned out I was wrong, and now comes the hard work to get things right", but only if all the precautions were done beforehand.

Why don't we have a "candidate" name-space, a separate library, like the contrib before, which most people who prefer to be on the cutting edge just include automatically. That would give reasonable feedback on how much traction new stuff like these get, and keeps it physically obvious what is considered The Core.

Regards,

Daniel

Michał Marczyk

unread,
Jul 19, 2013, 4:38:48 AM7/19/13
to clo...@googlegroups.com
On 19 July 2013 10:24, Daniel Dinnyes <dinn...@gmail.com> wrote:
> Why don't we have a "candidate" name-space, a separate library, like the
> contrib before, which most people who prefer to be on the cutting edge just
> include automatically. That would give reasonable feedback on how much
> traction new stuff like these get, and keeps it physically obvious what is
> considered The Core.

https://github.com/clojure/core.incubator

Cheers,
Michał

Alex Baranosky

unread,
Jul 19, 2013, 5:39:06 AM7/19/13