Fork me on GitHub
#cljs-dev
<
2019-05-08
>
mhuebert12:05:31

a little exploration of possible ways to support destructuring of js keys: https://dev.maria.cloud/gist/691ace12d14523337c7658ebb209c064?eval=true

Roman Liutikov13:05:51

@mhuebert reminded me of a similar approach I was looking into

(require '[goog.object :as obj])

(let [{:goog.object/keys [x y]} #js {:x 1 :y 2}
      {::obj/keys [z]} #js {:z 3}] 
  ...)

mhuebert13:05:55

Yeah, I was thinking along those lines with the :js-keys direction. The downside I see there is that unlike :strs, :syms and :keys there is no obvious way one could rename the binding. (Aside from the renamable keys issue, but that’s really the limitation of goog.object/get)

Roman Liutikov13:05:57

:js/keys also looks nice

souenzzo13:05:48

Clojure has no notation to access raw objects in java It will bring complexities/ambiguities to clj(c) ecosystem

mhuebert13:05:07

Yes, the .-key forms would need to be within reader conditional :cljs branches in cljc files. Incorrect usage would throw at compile-time, so you wouldn’t have runtime errors.

mhuebert13:05:13

Property access is a pretty low-level host concern and there are already lots of gotcha’s/differences between cljs and clj there.

bronsa13:05:13

clojure has bean

bronsa13:05:15

user=> (bean (java.util.Date.))
{:day 3, :date 8, :time 1557321124396, :month 4, :seconds 4, :year 119, :class java.util.Date, :timezoneOffset -60, :hours 14, :minutes 12}

Alex Miller (Clojure team)13:05:35

there's also https://github.com/clojure/java.data for a different take on same idea

souenzzo13:05:50

bean is kind of js->clj for advanced usages, we can do javascript.data

Alex Miller (Clojure team)13:05:51

but that's a data transformation approach, not access

mhuebert13:05:07

Yes, the .-key forms would need to be within reader conditional :cljs branches in cljc files. Incorrect usage would throw at compile-time, so you wouldn’t have runtime errors.

Alex Miller (Clojure team)13:05:34

.- is valid in clj too (for fields)

mhuebert13:05:38

Yes - one difference is that in clojure, unlike cljs, we can’t detect presence of a .-key at runtime (AFAIK), and destructuring forms assume support for this lookup/not-found semantic

mhuebert13:05:54

But I use cljs way more than clj so i am not so familiar with the java side

mhuebert13:05:11

I mean you could support {x .-field} destructuring in clojure, but it would necessarily behave differently than all other destructuring lookups, whereas in JS we can actually get the expected lookup semantic

mhuebert13:05:48

(Without using the ILookup protocol, so it should be a zero-cost convenience)

Alex Miller (Clojure team)13:05:53

yeah, no interest in doing that in Clojure

Alex Miller (Clojure team)13:05:34

b/c most fields are private in typical Java classes, this would rarely be useful

mhuebert13:05:13

I came to the same conclusion after asking around recently to see if it would make any sense at all for js-interop forms to compile to something sensible in Clojure (to work in cljc). Did not seem like a useful direction, though some folks have asked for it.

dnolen13:05:13

@mhuebert it's worth noting this has been suggested before, and I even toyed around with it myself in the old days (2012?), I even had a discussion with Rich about it - and I think in the end I agreed [with his conclusion that it's not a good idea]

dnolen13:05:58

as you say #2 and #3 are just non-starters. The problem with #1 is it's just "ugly" since it's really a special case

dnolen13:05:12

whereas destructuring normally works through protocols

dnolen13:05:26

so you're just shoving something in that doesn't really fit

dnolen13:05:12

also years of experience on projects and viewing other people's project has shown it's just not a big deal (certainly not coming up as a big ticket item on surveys) - so we put in something that doesn't really fit that only has minimal impact

mhuebert13:05:13

I didn’t really see this as a tractable problem until I got a handle on how to work with renamable vs static keys cleanly (eg checking for presence of a renamable key)

mhuebert13:05:20

Personally i would use it a lot, but i think it depends how much host-interop one does and how often one is in a perf-sensitive context

dnolen13:05:07

right I would set perf sensitive stuff to one side

dnolen13:05:26

writing moderately ugly code is par for the course

mhuebert13:05:39

By perf i just mean, how often one is working with raw objects and deftypes vs maps

dnolen13:05:02

if it's not tens of millions of ops a seconds, it's not perf

dnolen13:05:18

anyways for that stuff, not interested in making things easier

dnolen13:05:24

it's not meaningful for enough users

dnolen13:05:40

host-interop is the only angle worth arguing for

dnolen13:05:05

and thus far I don't see enough value - lots of folks are writing apps with Reagent, using Transit for interchange, with no interop to be seen

dnolen13:05:08

it would be useful to know about concrete APIs where you and others feel like this is paying its way

mhuebert14:05:03

One confounding variable is that the more inconvenient host-interop syntax is, the more likely one is to avoid raw interop and prefer wrappers or cljs-specific stuff

dnolen14:05:04

I don't think I've personally seen projects where I've found that to be true

dnolen14:05:21

people use the interop that's there - lots of stuff isn't worth wrapping

dnolen14:05:58

I've found that situation between Clojure & ClojureScript here to be hardly any different

dnolen14:05:06

^ my point above is simply

dnolen14:05:22

A) like Clojure, in ClojureScript you can write your core in an idiomatic style

dnolen14:05:45

B) interop stuff can be pushed to edges for the few things that you need, and those can be behind some simple fns calls to hide the details

dnolen14:05:20

no proposal is going to change the above

dnolen14:05:33

in fact, it will only make B) nicer - which again I'd argue isn't that interesting

mhuebert14:05:25

in my own conversations with other cljs devs, destructuring comes up a lot as a pain point

mhuebert14:05:42

it’s also a frequently commented-on and appreciated feature of js-interop (has a little lookup helper)

mhuebert14:05:58

so our experiences differ in terms of what we are hearing

lilactown14:05:01

my experience is with answering questions in #reagent / #shadow-cljs, and a lot of them have to do with people wanting to use JS components and doing JS interop. what I’ve seen is: 1. most people in the community go to great lengths to avoid doing “gross” JS interop-y stuff (at least at the syntax level) 2. this friction dissuades people from either adopting CLJS (because they’re coming from JS) or from using 3rd party JS libs, which is a net negative in terms of productivity in a lot of cases

dnolen14:05:05

well I follow along, and I don't hear about it

dnolen14:05:23

if people don't talk about it won't know - one proposal does not a movement make

dnolen14:05:52

1) just sounds like education problem

dnolen14:05:11

2) uh, I don't know what to say about that other than ... what?

dnolen14:05:42

but as you know I'm not sympathetic to these JS-centric opinions

lilactown14:05:47

I think (1) is more than education, it’s more about “I want to do Clojure(-y) things and this interop stuff doesn’t feel or look like Clojure”

dnolen14:05:20

that is exactly education

dnolen14:05:29

because that is exactly Clojure

lilactown14:05:45

it’s an emotional response, not a rational one, I guess is what I’m saying

dnolen14:05:47

people who think interop isn't Clojure - haven't written enough Clojure

dnolen14:05:57

and don't yet understand what it's about

dnolen14:05:08

please send them to watch these early Clojure talks

dnolen14:05:16

still relevant

dnolen14:05:32

literally nothing has changed here

mhuebert14:05:18

i think the parts of host-interop that work well, are part of what make clojure(script) attractive, so I don’t see why we would not want host-interop to be as clean as possible. for every 10 (or 100) crap JS libs there is 1 really good one that is nice to be able to use without a lot of wrapping effort

❤️ 4
mhuebert14:05:47

but in any case I am not in a rush and didn’t make this little exploration as a crusade 🙂.

dnolen14:05:18

hrm, I don't have much more to say here because we keep packing in too many assumptions in our statements (and I'm not helping clearly)

dnolen14:05:27

but wrapping is not idiomatic

dnolen14:05:34

start there please

dnolen14:05:07

doing something special for ClojureScript at the destructuring level is a big change

dnolen14:05:16

it better be a really good reason - so far I don't see it

lilactown14:05:50

I guess this hits close to home for me because the React wrapper I’ve been working on basically is just there for destructuring props + parsing hiccup. a lot of the positive feedback has come from how easy it is to use and build regular React components, with close to the same ergonomics as reagent

john14:05:04

@mhuebert can your proposal work as a lib or does it require a change to core destructuring?

lilactown14:05:11

I’m eager to delete all that code tho 😛

lilactown14:05:27

but if it doesn’t speak to you, I understand. I’m not going to get bent out of shape about it

dnolen14:05:31

@lilactown how is this relevant to your wrapper?

dnolen14:05:42

since that's work you did - not what someone else has to do?

mhuebert14:05:13

@john not sure if you saw the doc - https://dev.maria.cloud/gist/691ace12d14523337c7658ebb209c064?eval=true - it could be used as a lib, if one was willing to use non-core variants of whatever macro is wrapping destructure, eg fn defn let

john14:05:02

Hadn't finished the gist yet... k

lilactown14:05:27

ah, not sure I understand the question. I’m not trying to prescribe someone else do my work, just saying that I think I have experienced positive impact and feedback with this kind of thing

john14:05:05

Well, you could provide your own destructure macro and use it in place. Would that lose some performance benefits?

lilactown14:05:38

and it does impact perf (negatively) atm

mhuebert14:05:46

one issue is that you can’t use a destructuring macro in-place, you have to rewrite the containing macro. eg (let (destructure [my thing])) isn’t valid

john14:05:54

I guess I'm thinking of something more akin to your lookup thing, so perhaps I'm not fully understanding. I gotta give your gist a more thorough read later today.

john14:05:35

Seems like js-interop is a great place to incubate ideas and actually test the appetite of the community for these different kinds of ideas.

dnolen14:05:08

@lilactown right but I think we're tying to many topics together like I said 🙂

👌 4
dnolen14:05:33

there's nothing wrong with making beautiful wrapper that's going to sit at the core of a lot of projects

dnolen14:05:47

there's also nothing wrong w/ not wrapping random libraries

dnolen14:05:59

these two values are not in any conflict whatsoever

lilactown14:05:37

yeah, I expect I’d find more things to build into a wrapper. was just trying to relay my experience with providing this sort of destructuring API for JS objs

dnolen14:05:38

I would also take this is step further if we're going to use React as example

dnolen14:05:11

the point for me was never to "wrap" React - rather set it up so you could drive it with persistent data structures

dnolen14:05:19

which is tractable, and peforms well

dnolen14:05:44

most rando-libs don't work that way - there's no benefit to permit a persistent data structures API and a huge perf hole

dnolen14:05:42

so I understand even less the desire to wrap - since there's no win - other than some superficially nice looking API

lilactown14:05:50

I want to remove the need for wrappers personally. I think making the syntax nicer would help remove some of the impetus for them.

dnolen14:05:24

I don't really understand - destructuring doesn't have anything do w/ wrappers or no wrappers

dnolen14:05:46

anyways, gotta run - but I really think it's worth reviewing Rich Hickey's thoughts on interop a bit more closely

mhuebert14:05:51

it does, when the impetus for superficial wrappers is that sometimes working with js data structures is simply annoying. what @lilactown said earlier, that a lot of his code is written just to make it easier to destructure JS objects, and that his users appreciate that.

dnolen14:05:15

that's just one reading of what he's talking about

mhuebert14:05:19

if we rephrase “superficially nice” as “elegant and pleasing to use” we get much of the appeal of clojure proper. if you can work elegantly with host data structures with no need for wrapping, then why wrap? somehow we all agree that wrapping isn’t idiomatic clojure, we like elegant host interop, i guess it is a question of the cost of augmenting destructure and how far one wants to go

dnolen14:05:19

there are other readings

dnolen14:05:37

"design is not just how it looks"

mhuebert14:05:57

i think we can all agree on that

dnolen14:05:57

@thheller before I forget, re: the let thing - before you get too far let's make sure it's behind a flag (which may become a default) - completely dropping how we currently do things for var should be separate consideration step and probably much later down the line

dnolen14:05:01

@mhuebert all the other stuff aside, it's what I don't like about #1 - from a Clojure language design standpoint it doesn't really jive with what's there - Rich & I went around on exactly this point

favila14:05:40

I think part of the disagreement here js conflates "struct" and "dict" uses of objects

favila14:05:46

compare with clojure/java

favila14:05:58

java has a collections interface for "dict" uses

favila14:05:16

clojure absolutely supports that interface almost as well and natively as its own ilookup and nth

favila14:05:21

e.g. this is fine (let [{foo :foo} (HashMap. {:foo "bar"})] foo)

favila14:05:36

but clojure does not have "destructuring" of properties

favila14:05:07

in clojurescript/js, the situation is more confusing because of js's lack of distinction

mhuebert14:05:38

@dnolen fair enough, I see how the js side would be different from how all the other destructuring lookups work, and that it might not be desirable to have this in the language.

dnolen14:05:57

@mhuebert it's uglier than that

dnolen14:05:15

there's no protocol for this thing you want to do - so it's for a concrete case

dnolen14:05:26

destructuring right now is behind protocols so anyone can participate

favila14:05:07

google closure compiler makes this a bit worse because advanced compilation needs a syntax-level distinction

mhuebert14:05:09

right. as get is behind protocols, but .-field-access isn’t

dnolen14:05:19

not just get

dnolen14:05:29

seq destructuring too

lilactown14:05:36

even JS has issues with destructuring it’s own data because of it’s lack of distinction

let { someMethod } = someObj;

someMethod(); // => Error! `this` is undefined

favila14:05:06

hm, I think that's a different problem

lilactown14:05:04

one could imagine that if there was more of a distinction between a dict (or something with “lookupable” properties?) and an object that destructuring someMethod wouldn’t even be possible ¯\(ツ)

favila14:05:53

yeah that's true. it's more idiomatic in js to mix these uses

favila14:05:32

when they conceived of their object destructuring syntax they still did want it for destructuring arbitrary objects

favila14:05:06

but there's no js destructuring for x.get() (what Map would use)

lilactown14:05:23

yeah, and it does allow you to do funky things like:

let { someMethod } = objA;

someMethod.call(objB); // `this` is objB now

lilactown14:05:53

THAT (re: Map usage) is one of the biggest overlooked things in JS

lilactown14:05:07

no one uses Map because of it’s syntax

favila14:05:10

it came really late to the party also

favila14:05:19

after everyone's bad habits were set

lilactown14:05:47

they should have created a literal for it and allowed it to support destructuring

lilactown14:05:08

AFAICT Map would be much easier to optimize, and 80% of the time it’s what people actually want

favila14:05:32

this is also the biggest pain for google clojure adv compilation too, because it needs struct and dict uses to be separate and use separate syntax

favila14:05:50

struct uses need unquoted keys (not string access syntax) so it can rename; dict uses should always quote so it won't rename (it can cross a library or runtime boundary safely)

dnolen14:05:35

@mhuebert perhaps another way - I think records in Clojure have an optimization that allow field lookup (at call site)?

dnolen14:05:01

perhaps worth looking at how that works and that will lead to a better idea that requires no changes - and faster records

mhuebert14:05:16

do you mean field lookup by keyword?

dnolen14:05:27

yes Clojure does something here if I recall

dnolen14:05:51

if we do something for records and it's not a code size issue, maybe JS objects can piggieback

favila14:05:19

the LookupThunk

dnolen14:05:24

it may only be for (:foo ...) not sure if (get x :foo) is included

favila14:05:42

I think that's right

dnolen14:05:40

though I'm not sure why the form matters for destructuring

dnolen14:05:04

I can't think of anything obvious at the moment

mhuebert14:05:14

> there’s no protocol for this thing you want to do - so it’s for a concrete case > destructuring right now is behind protocols so anyone can participate to be clear the problem here is that it introduces a kind of impurity to the destructuring process - that it is no longer entirely protocol-driven - not that the property-access interferes with any existing lookups

dnolen15:05:11

that's right

dnolen15:05:18

currently destructuring is protocol driven

dnolen15:05:35

and what you're proposing is something that isn't destructuring in any previous sense of the term

dnolen15:05:17

@favila in general it has to be get

dnolen15:05:26

I see - yeah because of :strs etc.

favila15:05:51

IIRC it's specifically called a KeywordLookupThunk

favila15:05:03

@mhuebert you could explore a different syntax

favila15:05:23

the convenience of js syntax destructuring is what people are after, right?

favila15:05:39

js destructuring is specifically property destructuring

favila15:05:53

it's not a choice made by the thing destructured but by the destructurer

favila15:05:30

a let-props form which specifically did that, and didn't consult protocols?

mhuebert15:05:25

@favila certainly can do that - do you mean something different from what I did in the gist?

mhuebert15:05:29

i’m not sure how one could do something in destructure for JS objects that doesn’t involve adding specific, intentional syntax.. also because you need to be able to differentiate renamable vs static keys

favila15:05:30

the difference is only not delegating

favila15:05:07

js-let actually is what I'm thinking

favila15:05:06

ok, I wasn't thinking you could do both, but if your syntax is distinct enough you can

favila15:05:47

I didn't read it carefully but I'm not sure how you get away with calling (destructure) first instead of last

mhuebert15:05:55

yeah. it leverages the fact that .-dot-keys are both associated with host-interop, and not valid except in special circumstances, so i can use them here without any chance of breaking existing things

favila15:05:17

seems like you would need to pick out your syntax first and then feed the rest to destructure

mhuebert15:05:43

destructure just treats these like any other symbols

mhuebert15:05:52

letting destructure do its work first keeps it simple, and is particularly clean because i have a j/get with the same api as cljs.core/get, so i can swap them out at the end when recognizing a js lookup

favila15:05:15

that js-interop/get call seems to destructure with property access?

favila15:05:20

sorry, imprecise

favila15:05:33

with the string syntax, e.g. o["lookup"]

mhuebert15:05:20

js-interop/get supports keywords for static keys, ie string syntax, o["lookup"], or dot-keys for o.lookup

mhuebert15:05:29

the latter can be renamed by the compiler

mhuebert15:05:45

in this case it would be o.lookup

favila15:05:50

ok, so using a keyword emits o.lookup, using a string emits o["lookup"]

favila15:05:59

js-let doesn't support string lookup?

favila15:05:05

string property lookup

mhuebert15:05:02

using a .-dot-symbol emits o.lookup, keyword/string emits o["lookup"]. this js-let would emit o.lookup since it is using the .-dot-keys, these would be compiled to o["lookup"] if you had a ^js hint on the target object or if the property was in your externs

favila15:05:53

hm that seems a bit magic

favila15:05:31

I guess I could be careful to ^js any object made in cljs with #js{:foo "bar"}

favila15:05:20

anyway, this seems neat, I'm not sure of the value of mixing the two (i.e. js-let supporting normal let and also special js property destructuring) but it's something I might pull from a library sometimes

mhuebert15:05:52

i think that magic bit is just a consequence of externs inference

mhuebert15:05:19

nothing specific to what I wrote.

mhuebert15:05:56

I guess a specialized js-let could have a way of using keywords for static keys

lilactown15:05:00

I think there’s some opportunity to guide people in a particular direction

lilactown15:05:33

should people prefer static keys, or optimizable?

mhuebert15:05:39

That might depend on your tools. After I added support for renamable keys to js-interop, I’ve started using them more often and quite liking them. But in raw cljs can be pretty awkward just to create an object with a renamable key. Like

(let [o #js {}] 
  (set! (.-theKey o) 10) 
  o)

mhuebert15:05:23

vs (j/obj .-theKey 10)

mhuebert15:05:56

but I would think you might want to use renamable keys for the same reasons that all of CLJS does

lilactown15:05:59

yeah definitely

lilactown15:05:28

I think we should prefer renamable keys. unfortunately most of my code doesn’t use them 😛

favila15:05:15

renamable is better, but there's no cljs syntax to create them

favila15:05:32

(other than deftype/record, which brings other stuff with it)

favila15:05:02

one needs to always carefully consider which to use though

4