Nested pipe operators and js interop best practices

Hello,

I just realized that many of the cljs works well with js native arrays and objects. Things like filter and map operates well on JS arrays and some macros are a very convenient way to handle things.
This makes me question about two (I think) equivalent constructs:

(.. jsObject -prop1 -prop2)
(-> jsObject .-prop1 .-prop2)
(some-> jsObject .-prop1 .-prop2)

The obvious advantage of the latter is that if any of the property access returns a null value I will not receive an error trying to access a property of null.
According to your experience (and if such thing exists) best practices what is the more recommendable solution ?

I’m on a situation where I can use the last construct (using the some pipe) but I need to do it inside the scope of another similar macro. It works but I am not sure if it is a matter of luck or if it works fine because it is intended to be used this way:

(defn findServicesWithImage [imageName services]
             (->> services
              (filter #(=(some-> % .-launchConfig .-imageUuid) imageName))))
1 Like

Have you looked at cljs-oops? It is a library that deals with this, as well as removing the need for externs.

It allows you to use soft access (similar to some->) by starting a property name with ?.

2 Likes

Hello @mjmeintjes,
I know about that library, yes, but at this stage I want to use as few libraries as possible. At the end, for me cljs is just another layer over js, so I don’t want to add also a library to the mix.

At some point I may include that library in my workflow, but first I want to understand how things can be done using pure cljs

In that case I’d probably say that what you are doing with some-> is fine, as you long as you also maintain an externs file to ensure that the property names keep working in advanced compilation mode.

If I wasn’t using oops, I’d probably replace it by using some->>, goog.object/get and string property names, but that is just because I don’t like to think about externs.

In my opinion the Google Closure libraries are definitely part of core Clojurescript and well worth using.

Nobody likes to think about externs. In my opinion mangling object property names was an absolutely terrible idea from the Google team.
But in numerous case I’m targeting nodejs, where the maximum level of optimization is simple. However thank you for warning about it.

Regarding your preferred option using the goog.object library, how would you that exactly? As I can think about it you have to write the get method on every step, right?

All the arrow macros can be nested one inside another freely.

For interop, I personally prefer .. over ->, because I feel it conveys better the meaning that you are accessing things within an object, but I have been rethinking my position since I’ve started making heavier use of some->, as if using the latter I might prefer to go the route of consistent syntax.

Bottom line, both way is perfectly fine, I don’t believe one is more idiomatic then the other, so feel free to choose your favourite.

2 Likes

The rule of thumb I’ve heard is: If you’re treating the object as a hash map, use goog.object/get, but if you’re treating it as an object as in OOP, use the (.method obj) notation.

Also note that the goog.object namespace contains the equivalent of get-in for JS objects: goog.object/getValueByKeys. See here

3 Likes

Thanks all for all your comments. I was worried about unexpected behaviors because the nested macros, but If I can merge and mix them freely (not becoming too fancy) I think I have my decision taken.

@pesterhazy I think you have posted the wrong link. Thank you for mentioning it though, can I make (getValueByKeys "a" "b" "c") or (getValueByKeys "a.b.c") or even (getValueByKeys "a?.b?.c") ?

As I said, I would prefer to avoid libraries, but since google one is already built-in I’m ok with it

@danielo515 the link looks correct, did you read the whole page?

As for how getValueByKeys works, try it in the REPL:

$ lumo
Lumo 1.7.0

cljs.user=> (goog.object/getValueByKeys #js{"a" #js{"b" #js{"c" 1}}} "a" "b" "c")
1
cljs.user=> (goog.object/getValueByKeys #js{"a" #js{"b" #js{"c" 1}}} "DOES-NOT-EXIST" "b" "c")
nil
1 Like

Just want to point out, while they can be nested as you want, they also behave exactly as their docstring says, so some form of nesting just aren’t as useful, such as nesting -> inside ->>.

(->> 100
    (-> (- 10)))

This means 100 will be the last argument of -> and so it will result in:

(-> (- 10) 100)

Which is logical, but sometimes surprising. You might expect them to change the way they work within one another, but they don’t, to ->>, -> is just another function you are asking it to append an argument to before it runs.

This leads to an important Clojure basic. Nested macros expand from outermost first to innermost last.

2 Likes

Thank you very much for your detailed explanation. You saved my potentially headaches

Instead of breaking my head each time trying to mentally macro-expand nested threading operators, I use this rule of thumb: only nest when the outside operator is thread-first (->, some->, cond->). Since all threading macros take their input in first position, this always works.

A particular pattern I’ve encountered a few times is cond-> inside ->

before:

(cond-> x
  true  do-this
  true do-that
  (condition? x) do-cond
  true do-final)

after:

(-> x
    do-this
    do-that
    (cond-> (condition? x) do-cond)
    do-final)
3 Likes

Thanks for your contribution.
I can see how your example is better on the second case, but I don’t understand why do you need the cond-> macro at all. Could you expand on it ?

This is for the case where you want to do a sequence of transformations, but one of them only needs to happen in some cases. In the example above, do-cond only needs to happen when (condition? x) is true, the other operations need to happen every time.

I’ve tried various approaches to do this over time, and finally settled on this “cond-> inside ->”.

A less “clever” way to do the same thing would be something like this.

(let [result (-> x do-this do-that)
      result (if (condition? x) (do-cond result) result)
      result (-> result do-final]
  result)

You could also do something like this

(-> x
     do-this
     do-that
     ((if (condition? x)
        do-cond
        identity))
     do-final)

but that just makes me think too hard about how -> expands.

I hope that makes it more clear, sorry for hijacking this thread a bit.

To get back to your original question

.. is great when you’re doing a lot of interop, but it does have one downside: you can’t use your own ClojureScript functions as part of the chain, so the chance is high that at some point you’ll have to change that .. to a ->. There’s not much lost with just always using -> instead.

As for -> vs some->, while the latter is very useful, you shouldn’t overuse it. If you’re dealing with a third party library handing you possibly incomplete data then it’s a great fit, if you’re using it on your own data, then instead try to write your code in such a way that you avoid having nil values pop up in unexpected place. This will make your code more “confident” (it’s not constantly second-guessing itself).

This concept is from Avdi Grimm’s excellent book, “Confident Ruby”, but it can also be applied to Clojure.

1 Like

Thanks for expanding on your explanation.
I see returning a nil value inside a pipe as a good way to short-circuit the logic. As you said, is a better idea to write my own code in a way that nil values are not a surprise but just a controlled case.

Regarding the cond macro, I understand that it accepts a value as its first parameter, in this case it is X because that is what -> is injecting. If so, what is the need of using (condition? x) ? the X should be implicit at some point of the cond macro, that’s the only point I’m not fully understanding.

Regards

I see now that it’s a confusing example. The x is implicit for do-cond, but not for (condition? x). That’s because cond-> is of the form

(cond-> input
  condition-1 operation-1
  condition-2 operation-2
  ,,,)

In the right hand part you can omit the argument, it will be “threaded” through by the macro, but the macro doesn’t change the left hand part. If any of those conditions need arguments, then you need to write them, even when they are the same as the input.

Maybe some more concrete code will help? Here’s a hypothetical function for generating some HTML, this is the kind of thing where I can imagine using a cond->inside a ->.

(fn [{:keys [extra-classes message warn? error?]}]
  (let [classes (-> ["message"]
                    (concat extra-classes)
                    (cond-> warn? (conj "warning")
                            error? (conj "error")))]
    [:div {:class (str/join " " classes)} message]))

Thank you again for your examples @plexus
With your examples and after re-viewing the documentation of the cond-> macro for third time now I understand it.
Your first example was perfectly fine and clear, my lack of a correct understanding of the cond macro was the actual culprit. Let me explain with code what I think I understood. Here is how I think that macro expands, well, not how expands but a visual explanation of how the term is inserted and by who

(-> x  ; I'll write XA where -> inserts the X and XC where cond inserts the X
    (do-this XA)
    (do-that XA)
    ; The first macro inserts the x on cond and then cond on it's nested body
    (cond-> XA (condition? x) (do-cond XC)) 
    do-final)

Regards

Don’t forget as->. That one is great to use as the outermost, since everything can be nested usefully in it. Also useful to nest inside another thread-first if you need to pipe to a middle argument, since as-> is the only one that I know that supports middle arguments.

1 Like

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.