Fork me on GitHub
#clojure-dev
<
2021-09-27
>
chrisblom09:09:26

Can anyone explain the rationale behind (get 1 :a) returning nil? I was under the assumption that (get m k) was equivalent to (val (find (m k)) , but i just discovered that this is not the case when m is not associative or a map

delaguardo10:09:15

(get 1 :a) equivalent to (get 1 :a nil) meaning “return default value if the second argument is not a key in associative collection or first argument is not an associative collection”

chrisblom10:09:09

i've checked the definition in RT.java, i was wondering why it does not throw on non-associative args, like find and contains? do

chrisblom10:09:56

this surprised me, get not throwing on non-associative args seems very error prone to me, so i was wondering if this is a bug, or if there is a good reason to return nil

chrisblom11:09:10

okay, after checking what happens when it would throw, I see it breaks some things, for example in in clojure.spec.alpha there is some breakage here:

(defn- accept? [{:keys [::op]}]
  (= ::accept op))
as this fn gets called with non associative args, and the destructuring expands to (get map_9996 ::op)

borkdude11:09:13

this begs the question: should keys destructuring throw when passed a non-map arg?

chrisblom11:09:58

i would say yes, but there seems to be a lot of code out that assumes otherwise

borkdude11:09:05

probably ship has sailed, but it'd be interesting to know the reasoning. it is documented though in the docstring of get that it behaves this way (one could argue)

chrisblom11:09:53

(doc get): Returns the value mapped to key, not-found or nil if key not present. It is not very explicit though

chrisblom11:09:18

might be good to add something like , or map is not associative.

borkdude11:09:14

get doesn't work only for associative types though. it works for strings for example. And it can be extended with ILookup

ghadi12:09:12

this is absolutely by design and intentional

ghadi12:09:15

if you have something that returns a map or a scalar, you can preemptively destructure it or get from it, even when it returns a scalar

ghadi12:09:53

(let [transact! (fn [xyz]
                  {:db-after :map}
                  ::anom/conflict)
      {:keys [db-after]} (transact! xyz)]
  (if db-after
    ...))

chrisblom13:09:29

is it by design or is this accidental? In contrast, sequence destructuring throws when destructuring a non-sequential value, for example (let [ [a b c & more] :scalar ] a) throws java.lang.UnsupportedOperationException: nth not supported on this type: Keyword

chrisblom13:09:48

hmm, i'm digging trough clojure.spec.alpha, where map destructuring is used in the way ghadi mentions several times, so i guess it is intentional

chrisblom13:09:35

still, it seems very error prone to me, but maybe i've spent too much time in static typing land lately

chrisblom13:09:52

cool, i didn't know :keys destructuring will also destructure seqs: (let [{:keys [a b c]} '(:a 1 :b 2 :c 3)] [a b c]) => [1 2 3]

borkdude13:09:22

@chris.blom it does. also in 1.11:

(let [[& {:keys [a b c]}] [{:a 1 :b 2 :c 3}]] [a b c])
[1 2 3]

borkdude13:09:09

the use case for that is primarily allowing maps to be passed to key-named argument functions

vncz22:09:14

What was the previous way before having this system? I recall there was another one

borkdude22:09:40

something with apply and mapcat

vncz22:09:28

It has to be somewhere in spec, because they do something like (s/keys :req-un [::a ::b ::c] :opt-un [::d ::e])

vncz22:09:37

@U064X3EF3 I recall you already explained me this and what changes for 1.11, would you mind saying that again quickly?

seancorfield22:09:33

@U015VUATAVC

(! 837)-> clj -A:1.10
Clojure 1.10.3
user=> (defn f [& {:keys [a]}] a)
#'user/f
user=> (f {:a 1})
Execution error (IllegalArgumentException) at user/f (REPL:1).
No value supplied for key: {:a 1}
user=> (f :a 2)
2
user=> ^D

Tue Sep 28 15:29:56
(sean)-(jobs:0)-(~/clojure/fresh)
(! 838)-> clj -A:1.11
Clojure 1.11.0-alpha2
user=> (defn f [& {:keys [a]}] a)
#'user/f
user=> (f {:a 1})
1
user=> (f :a 2)
2
user=> 
☝️:skin-tone-2: That is the specific change in Clojure 1.11.

seancorfield22:09:40

This is what folks used to have to do to "compose" such calls prior to 1.11:

user=> (apply f (mapcat identity {:a 1}))
1

vncz22:09:16

Oh ok so the specific change is the idea that you can use the same syntax with both a map and a vector with values inside @U04V70XH6

vncz22:09:02

No? I can see that the same function is not able to work with both a map and the arguments passed as key value params

vncz22:09:26

Yeah sorry I did not mean vector, I meant “in the args vector, one after the other”

vncz22:09:33

{:a 1} same as :a 1

seancorfield22:09:34

These have always been different things:

(defn f1 [{:keys [a]}] ..) ;; accepts a single hash map as an argument
(defn f2 [& {:keys [a]}] ..) ;; accepts key/value named argument pairs

seancorfield22:09:17

With 1.11, you can now use the second form everywhere and call it with either named argument (key/value) pairs -- as before -- or with a hash map (new)

vncz22:09:02

Yes that’s what I meant. The latter is compatible with the former

vncz22:09:14

There was an unhappy wording on my side

seancorfield22:09:27

In particular, this hybrid functionality is completely new:

user=> (defn f [& {:keys [a b c]}] (println a b c))
#'user/f
user=> (f :a 1 :b 2 :c 3)
1 2 3
nil
user=> (f :b 2 {:a 1 :c 3})
1 2 3
nil

vncz22:09:17

@U04V70XH6 Do you happen to know what’s the rationale of using :a 1 :b 2 instead of a regular map?

seancorfield22:09:18

This makes such functions much more composable because you can add new positional arguments in a given call as well as passing through an existing hash map of arguments.

seancorfield22:09:54

@U015VUATAVC A lot of people think it's more "human-friendly". Hence the blog post I linked to above.

seancorfield22:09:19

(foo :this "value" :and "some" :other "value") is considered a "human interface"

seancorfield22:09:55

(foo {:this "value" :and "some" :other "value"}) is considered a more programmatic interface -- and such calls compose much more easily.

vncz22:09:14

Understood

seancorfield22:09:55

I was excited enough about this new feature to immediately upgrade to 1.11 at work and we have code relying on this in production already.

seancorfield22:09:20

(we have code relying on update-keys and update-vals in Alpha 2 in QA right now as well)

vncz22:09:47

Yeah I recall you jumping on the ship early. You’re the official alpha tester here apparently troll

vncz22:09:55

If you say it works, then it works

seancorfield22:09:22

We've run alpha builds in production for over a decade at this point...

👍 1
vncz22:09:08

Last thing I need to figure out is my IDE. Right now I am on VSCode and Calva but I think it’s not the best. I have tried Sublime and Spacemacs but still, I can’t find something that fits 😞

seancorfield23:09:08

I've gone through a lot of editors and IDEs and ended up at VS Code + Calva + Clover (because I don't use nREPL). What are you finding doesn't work for you about VS Code / Calva?

vncz23:09:02

It hangs continuously and I have no idea why

seancorfield23:09:44

I have always started my REPL outside my editor and just connected the editor to it -- my REPLs far outlive my editor sessions and often run for weeks, whereas I have to restart my editor for updates far more frequently.

seancorfield23:09:42

And, yes, that is what I use -- with a Socket REPL which you have to start yourself outside VS Code -- and you have to disable Calva's nREPL UI (via settings) so that it doesn't get in the way.

seancorfield23:09:15

We have Socket REPLs running in our QA and production processes too -- so I can use VS Code to work on "live" processes if I want.

vncz00:09:35

@U04V70XH6 Why not nREPL, out of curiosity

seancorfield00:09:47

a) I want the same editor/REPL experience regardless of which project I'm working on and whether the process is local or remote b) I don't want unnecessary dependencies in my deployed processes c) I want to be able to start up any Clojure process (or a non-Clojure process that uses Clojure as a library -- as is the case for us at work) and tell it to start a REPL on a given port, with no code needed and no dependencies needed. The Socket REPL satisfies all three of those requirements at the same time.

borkdude13:09:57

Clojure 1.11.0-alpha1
user=> (defn foo [& {:keys [a b]}] [a b])
#'user/foo
user=> (foo :a 1 :b 2)
[1 2]
user=> (foo {:a 1 :b 2})
[1 2]

chrisblom13:09:06

yeah was a nice addition, really makes using [ & {:keys [a b c]} ] much more useful

vncz22:09:14

What was the previous way before having this system? I recall there was another one