Fork me on GitHub
#clojure-dev
<
2019-06-15
>
john15:06:37

Does using something like %:some-map-key for destructuring maps in anon fn args sound like a reasonable idea?

john16:06:04

Or at that rate, %[... and %{... with their destructure semantics

bronsa16:06:26

neither of those would be a valid symbol

bronsa16:06:58

so there would need to be special support at the reader level just for some special syntax that doesn't really readability

danielneal16:06:16

*improve readability

john16:06:45

right, % can be a valid symbol. So the existing variants don't break things syntactically

bronsa16:06:37

: is not a valid charcter for as ymbol, and [ or { can't be part of symbols either

bronsa16:06:02

but even if you picked some other syntax that was a valid symbol, I'd still say not a good idea

bronsa16:06:27

it doesn't buy you much and adds extra special syntax

gfredericks16:06:59

anything that's about anon-fn-args would have to be implemented in the reader anyhow so "you'd have to do it in the reader" isn't...you know...like...the objection it sounds like...or whatever

john16:06:35

Well, so a smaller change would be to simply allow %some-map-key to destructure a map key from a passed in map

john16:06:14

that's a valid symbol and could be contained in the fn* logic that picks out the indexed args

gfredericks16:06:16

or maybe it wouldn't; I guess you could do it with a macro

gfredericks16:06:39

the current stuff just happens to be implemented in the reader

john16:06:07

It does add extra semantics, but it'd sorta match up the sugar anon syntax with the rest of the language of being able to deal with both indexical and associative parameters

gfredericks16:06:25

(defmacro % [& crazy-fn-args] ...)

(% (+ %:foo %:bar))
can do it in userland

john16:06:31

would that work with anon fns though?

bronsa16:06:39

@gfredericks well there's no "special" syntax for #(), % is read as a regular symbol and interpreted in a special way

gfredericks16:06:11

@bronsa the reader expands it based on the largest %400 present

gfredericks16:06:51

I mean I'm sure we both understand what's going on and are only going to argue about what you meant by "special"

gfredericks16:06:13

@john I don't understand your question

bronsa16:06:56

% is intercepted at read time before it reaches macroexpansion time

bronsa16:06:06

so if % was a macro it wouln't nest the same way as it does(n't) now

john16:06:32

@gfredericks oh, I see what you mean, like using that macro instead of using #()

gfredericks16:06:34

okay you're just saying the name % is bad

bronsa16:06:43

any other symbol would do

gfredericks16:06:46

@john yes, though as @bronsa mentions it maybe needs a different name

gfredericks16:06:04

I believe you'd need a similar "can't nest them" restriction as #(), though I don't know how you'd detect/enforce it

bronsa16:06:46

user=> (let [x (with-local-vars [x nil] x)] (defmacro can't-nest [body] (if (bound? x) (throw (Exception. "can't nest")) (with-bindings {x nil} (macroexpand body)))))
#'user/can't-nest
user=> (can't-nest 1)
1
user=> (can't-nest (can't-nest 1))
Syntax error macroexpanding can't-nest at (REPL:1:1).
can't nest

bronsa16:06:49

(you'd need to macroexpand-all nestedly, but you get the idea)

gfredericks16:06:01

is the worst part about this that macroexpand-all doesn't work?

bronsa16:06:32

you could use t.a.jvm/macroexpand-all or ridley if you really felt inclined

bronsa16:06:07

i suspect it could massively break when used in combinations with some crazy macros like go though

bronsa16:06:33

not that i'd think it was a good idea even if it didn't break, was mostly a joke

gfredericks16:06:23

I guess that's the best part about implementing the base parse in the reader -- detecting nesting cleanly

bronsa16:06:17

yeah, reader macros have the nice property of expanding in the "opposite" order of regular macros

bronsa16:06:13

not really opposite but can't think of a better way to say it

bronsa16:06:09

anyway this is going a bit off topic, sorry

gfredericks16:06:37

they sorta do

john16:06:40

ah and the %some-key syntax wouldn't work because numbers and the & symbol are valid keys too

john16:06:12

I didn't think symbols with colons in them were allowed but that apparently works

bronsa16:06:57

loads of theoretically unreadable symbols are accepted by the reader

john16:06:22

That's specifically not supposed to be allowed though right?

bronsa16:06:25

%:foo is not a readable symbol, but the reader accepts it anyway

bronsa16:06:01

yeah, I'm pointing out that just because "it apparently works", doesn't mean it should be used

bronsa16:06:29

although I'm not sure what the reader allows and disallows is ever going to change so that may not mean much in practice

john16:06:42

All it really saves you is from having to do (:foo %)... not much. But I don't know, this seems pretty clojurey to me #(Point. %:x %:y %:z)

gfredericks17:06:56

@bronsa I like the idea of using the terms "de facto" and "de jure" to distinguish these cases 🙂

gfredericks17:06:10

dunno if I can persuade anybody else of that though

gfredericks17:06:07

@john there's also some weird asymmetry in the fact that you presumably can only reference the keys of the first arg, but you can otherwise access other args

john17:06:39

%4:y 😉

gfredericks17:06:50

I was afraid that would happen

😂 4
john17:06:58

But yea, asymmetric

john17:06:31

and %4:y is a little ugly to my current sensibilities

dpsutton17:06:03

Just from a usability side I would hate deciphering code like that. I don’t like complicated #(...) forms

john17:06:58

From another angle, one might argue that this is more self documenting #(Point. %:x %:y %:z)

john17:06:40

But yeah, %4:y is starting to look a little like perl or something

john17:06:30

But it's only one level deep... not that confusing :thinking_face:

gfredericks17:06:51

you mean it's not that deep 😉

john17:06:51

It's not a second or third order that, is all I'm saying

john17:06:58

What does that even mean anyway lol... anyway, good chat. Maybe I'll try the idea out sometime and report back

didibus18:06:18

Speaking of which, what's the reason for not allowing #() to nest?

gfredericks18:06:11

well % becomes ambiguous; I can't think of any severe ambiguities/problems that would arise from an explicit "inner #() can never reference args from any outer #()" rule

andy.fingerhut18:06:42

I wouldn't be surprised if the fact that (fn [...] ...) isn't very many more characters, and does not have the restriction or extra-rule-to-learn overhead for reading and understanding, factored into the decision somewhere.

andy.fingerhut18:06:52

But I didn't design it, so those are just guesses on my part.

gfredericks18:06:06

yeah it's definitely the more conservative choice; easy to relax the rules later, hard to enstricten them

john20:06:10

So I tried to do something in user land using just tagged readers, like #m/% #(Point. %:x %:y %:z) or #(Point. #m/% :x ... and neither work 😕

gfredericks20:06:01

that looks makeworkable what's it do?

bronsa20:06:59

% is interpreted too early in the reader in #() for the first to work

bronsa20:06:52

the second may work

bronsa20:06:07

unless the transformation fro #m/% :x to (:x %s) happens too late in the reader, could be

gfredericks20:06:36

oh I see, the reader won't just pass %:x through

gfredericks20:06:44

you could imagine it being implemented more leniently

bronsa20:06:59

user=> (binding [*data-readers* {'m/% (fn [k] (list k '%))}] (read-string "#(Point. #m/% :foo)"))
(fn* [] (Point. (:foo %)))
yeah, in the second the transformation happens too late

bronsa20:06:31

the reader would need to "re-read" the form after the tagged reader has run

bronsa20:06:47

since it only interprets literal % symbols in a #() context

bronsa20:06:15

quite a subtle edge case

bronsa20:06:03

not even sure it's intentional, I'd say it's just a byproduct of the LispReader implementation

bronsa21:06:43

could definitely be made to work, not sure if it's worth the effort

didibus21:06:17

Right... so it's mostly an arbitrary restriction. I see. I've slowly started to just rely more and more on fn exclusively. There's still the occasional #() when there's a need for a short single fn call, since you don't need to duplicate parens. But I'm guessing the restriction is actually to kinda force people into this pattern

john21:06:35

I may have done it wrong, but

(#(println #m/% :bob) {:bob 1})
WARNING: Use of undeclared Var lz.core/% at line 1 <cljs repl>
nil
nil

bronsa21:06:09

that's compiling to (fn [x] (:bob %))

bronsa21:06:13

which is wrong

bronsa21:06:37

there's no way to make it work correctly with #()

bronsa21:06:58

#() interprets % args at the char level not at the sexpr level

bronsa21:06:16

so it's too early for any custom reader macro, which returns sexprs

bronsa21:06:20

(well, edn)

bronsa21:06:42

and too late for the other way round

john21:06:43

So to try it out, I'd have to play with the reader

bronsa21:06:55

no, you'd have not to use #() at all

john21:06:36

I mean, I'd have to change the logic that handles the char level reading of #() forms in clojure

gfredericks21:06:41

and then you have to write your own walking code and decide what to do about nesting

bronsa21:06:56

something like #f/% (foo #v/% :foo) could be made to work

bronsa21:06:03

but the implementation would be extremely complex

bronsa21:06:12

and you're getting more verbose than the alternative which already works :)

gfredericks21:06:19

@bronsa data readers and macros are equivalent approaches here, right?

bronsa21:06:29

kinda sorta not really

bronsa21:06:42

but you're getting into insane edge cases territory if you want to know the difference

parens 4
gfredericks21:06:27

is there an emoji with a face expressing half horror and half eager anticipation

bronsa21:06:37

hah, not that fun

bronsa21:06:08

if it were a macro, other macros would see the unexpanded macro expression vs the fn expression for one

bronsa21:06:17

and that's a problem if your wrapping macro codewalks

bronsa21:06:54

the other problem is more subtle and I can't give you a concrete case where it may break off the top of my head, but it exists and it has to do with the fact that fn* are compiled at analysis time

bronsa21:06:47

this is all supposing that you're trying to use that macro exactly as you'd use #()

bronsa21:06:57

it's a similar thing that of course exists with fn being a macro

bronsa21:06:17

but if you're asking if they're equivalent, then this does exist

bronsa21:06:37

it's just never going to ever be a problem to anybody ever unless they go look for it though :)

bronsa21:06:25

interestingly this is one of the reasons why in common lisp lambda is special cases in both the compiler and the reader

didibus21:06:54

Isn't it just that the code is first read, then tagged literals are applied. Then reading is done, and now macros are applied? So % being part of the reading means it's already been modified?

bronsa21:06:52

yes but if you're implementing #() as a macro rather than a reader macro which is what @gfredericks was asking, then it would not be a problem

bronsa21:06:13

what you're saying is kinda the reason why @john reader macros don't work

didibus21:06:29

Right, cause you could plug yourself before it is applied then correct?

didibus21:06:33

I think for @john it be easier he just went with a different tag. Like #f(Point. %key1 %key2). That would be possible I believe

bronsa21:06:12

it's what I was suggesting with #f/% (foo #v/% :foo)

didibus21:06:11

Haha ya I see. I think your choice of namespace confused me 😉

bronsa21:06:50

heh, implementing the args via another tagged reader would be slightly easier than using %key, but both approaches could work

didibus21:06:06

I went down a similar rabbit whole a while ago. And realized there's pretty much nothing shorter then just fn. Everything else is barely saving you anything hehe

bronsa21:06:08

yours requires code walking of the body though, the tagged reader version wouldn't

didibus21:06:44

Ah ya, I see that's true

bronsa21:06:06

the tagged reader version would be a bit of a mindfuck too, for ad ifferent reason: the args would be read before the fn tagged reader function would be invoked

bronsa21:06:20

so they'd have to register to the function, not the other way round which is the intuitive impl

john21:06:35

Then you have to reimplement #()s logic for %numbers, which I was trying to flow through

bronsa21:06:55

it's really not worth it :)

bronsa21:06:16

fun to think about though

didibus21:06:41

Ya... but what if your key is 1 😋

john21:06:22

I was thinking :4:ans/akey... Another illegal form :)

didibus21:06:10

Another plus of (fn), which I've started to use over #(), is you can name the anonymous fn, which helps a lot when exceptions are thrown

âž• 4