Fork me on GitHub
#beginners
<
2024-06-26
>
Melody02:06:41

How important is destructuring to understand as a practice? Is it a common technique? I do not understand it at all. Clojure for the Brave and True says "it lets you concisely bind names to values within a collection" and I read that and all I can think is like "okay is it assigning a variable name?" I think the answer is a resounding no, but I have no idea what "bind names to a collection" means other than to say the word "assign" or maybe "relate" names to a particular collection. In the Brave and True example it goes on to show latitude and longitude examples. It just seems to me like it is taking user input or something.

Noah Pederson02:06:55

It's more of a matter of taste, but it's fairly common in my experience. Destructuring is more like efficiently binding some inner value of a larger datastructure to a variable so that it may be used directly

phronmophobic02:06:00

I would say it's pretty common. One way to investigate what destructing does is to look at the code it expands to: destructuring a vector

> (macroexpand-1 '(let [[a b c] my-vector]))
(let*
 [vec__103250
  my-vector
  a
  (clojure.core/nth vec__103250 0 nil)
  b
  (clojure.core/nth vec__103250 1 nil)
  c
  (clojure.core/nth vec__103250 2 nil)])
destructuring some map keys:
> (macroexpand-1 '(let [{:keys [a b c]} my-map]))
(let*
 [map__103255
  my-map
  map__103255
  (if
   (clojure.core/seq? map__103255)
   (if
    (clojure.core/next map__103255)
    (clojure.lang.PersistentArrayMap/createAsIfByAssoc
     (clojure.core/to-array map__103255))
    (if
     (clojure.core/seq map__103255)
     (clojure.core/first map__103255)
     clojure.lang.PersistentArrayMap/EMPTY))
   map__103255)
  a
  (clojure.core/get map__103255 :a)
  b
  (clojure.core/get map__103255 :b)
  c
  (clojure.core/get map__103255 :c)])

phronmophobic02:06:41

A simplified version of the above:

;; more or less equivalent
(let [[a b c] my-vector])
(let [a (nth my-vector 0 nil)
      b (nth my-vector 1 nil)
      c (nth my-vector 2 nil)])
;; note the 3rd argument to `nth` means it will return nil if the index outside of the bounds of `my-vector`


;; more or less equivalent
(let [{:keys [a b c]} my-map])
(let [a (:a my-map)
      b (:b my-map)
      c (:c my-map)])

phronmophobic03:06:31

There are some differences between the simplified version and what destructuring does, but hopefully, it illustrates the idea.

Godwin Ko03:06:09

other than convenience, we use destructing as a kind of function parameter contract which help to document/describe what data within which input structure that is required for the function to manipulate

Melody03:06:56

I don't think I understand what "bind" means. I just keep seeing that word when I read about it. Bind sounds so simple but yet I don't know technically what a bound value means other than to assign it. I am beginning to think that perhaps destructuring is the type of thing "that I have been doing all along" that I didn't know the word for. Any time I write a function and I provide default arguments [x,y] to the argument, I am saying that the first value passed in is going to be treated as x in the scope of the function, and the second one y; rather than taking the time to declare each let statement on separate lines. Is this considered just "syntactic sugar"?

phronmophobic03:06:09

> Is this considered just "syntactic sugar"? Yes, exactly!

Melody03:06:27

Okay then I do think I understand and I am probably just overthinking things, as I have a tendency to do. Thank you all for the assistance 🙂

phronmophobic03:06:46

I don't think I understand what "bind" means.I'll probably butcher the definition, but I'll try anyways. Binding is just giving a name to a value so that you can refer to it later. It's very similar to assignment, but bindings can't change. You can rebind the same name to a new value though. This seems like it's similar to assignment, but it's not quite. For example:

(let [a 42
      lock (Object.)
      locking-println (fn [& args]
                      (locking lock
                        (apply println args)))]
  (future
    (let [a 43]
      (locking-println "inner a" a)))
  (locking-println "outer a" a))
If a was a variable instead of a binding, then assigning to a would have a race condition here and the outer print might print 42 or 43.

Melody03:06:02

That is a helpful distinction between the two thank you!

phronmophobic04:06:28

Although I guess bindings can change (even if it’s less common), see https://clojuredocs.org/clojure.core/binding

growthesque06:06:46

"bind" means associating a name to a memory block. for example when you say (def "my-var" 12) you are saying create a reference between the name I made "my-var" to the memory block that contains the value 12. It's kind of like creating a desktop shortcut. the confusion probably comes from videos which talk about how variable assignment is like putting a value in a box which isn't true for many languages including javascript and python. instead you are simply creating pointers to boxes (memory blocks). for example if you are more familiar with python:

>>> a = 5
>>> b = a
>>> c = 5
>>> id (a)
124006103200968
>>> id (b)
124006103200968
>>> id (c)
124006103200968
here the function id shows you that all names are "bound" to the same memory block even though you performed three separate assignments. this is because immutable values don't change and can be reused to save memory. similarly in clojure:
user=> (let [a 12
             b a
             c 12]
(println (identical? a b))
(println (identical? a c)))
true
true
nil
a small caveat: the reason why a and c are identical in both examples even though they aren't explicitly associated like b and a is because of something called integer interning where basically the runtime/repl pre-loads certain commonly used integers (-5 to 256 i think) in memory so that you can use them more efficiently. using something like 1024 for example will result in a and c not being identical anymore, but a and b still will be, which is the more important part. in other words in both python and clojure, assignment never copies data. and this is what "binding" means, that you are just pointing to things, creating relations, that names don't store values themselves (supposedly this is different for languages like C, but I don't have experience there). as far as I understand, in clojure due to something called structural sharing when you do (def vec1 [1 2 3]) and (def vec2 (conj vec1 4)) even though vec2 will return a new collection [1 2 3 4], the [1 2 3] part of it will actually be the same chunk that's bound to vec1. this way you don't have to store [1 2 3] twice in memory, just point to it from two different places (pointers take less memory than values). binding might be more similar to the way things work in real life I think. for example when you name someone "John" you don't store that person in "John" you are just associating the name "John" with that person. and so with destructuring, the way I understand it, you are simply providing a pattern that says, bind those names to those values.

exitsandman07:06:36

Hello. I'm trying to deploy to Clojars using deps-deploy (effectively with the deps-new build script) but I keep getting invalid-token rejections. I have created an account and a token, set the environment variables CLOJARS_USERNAME and CLOJARS_PASSWORD respectively to my Clojars username and the token, and then ran clojure T:build deploy . My pom.xml looks like https://pastebin.com/KTkaWHuV, the token like CLOJARS_1234.... I tried this process both in windows and with WSL, to no avail. Am I missing something?

lread11:06:11

maybe re-ask in #C0H28NMAS

Jim Newton08:06:32

Can someone point me to a document that explains how swap! works. I’ve read several explanations, and they all leave out important information. For example the explanation in Clojure for the Brave and True, and the explanation in the clojure doc https://clojuredocs.org/clojure.core/swap!

growthesque08:06:40

for all details, best way is probably to look at the actual implementation here: https://github.com/clojure/clojure/blob/clojure-1.11.1/src/clj/clojure/core.clj#L2362

daveliepmann08:06:01

What important information do you mean

Jim Newton08:06:15

the missing information in my mind is that at some point the user’s function has been called with the old value from the atom. Then a new comparison is made with the actual content of the atom. if the values is still the same, then the new value is set. But the value can still change between check and set. How that works is missing from the explanation.

daveliepmann08:06:09

> Internally, https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/swap! reads the current value, applies the function to it, and attempts to compare-and-set! it in. Since another thread may have changed the value in the intervening time, it may have to retry, and does so in a spin loop. The net effect is that the value will always be the result of the application of the supplied function to a current value, atomically. However, because the function might be called multiple times, it must be free of side effects. https://clojure.org/reference/atoms

Jim Newton08:06:17

ah so the magic is in compare-and-set! ?

Jim Newton08:06:40

is compare-and-set an atomic jvm operation?

Jim Newton08:06:45

or does it use locks?

Jim Newton08:06:25

Maybe it is obvious, but I’m trying to understand it well enough to explain to my students.

Jim Newton08:06:41

I could say, it just works, don’t worry about how.

daveliepmann08:06:10

compare-and-set is def atomic, but i don't think that's at the jvm level, looking now

exitsandman08:06:36

In the implementation you're likely using, it should works with a native procedure that eventually calls a bespoke CPU instruction, no locks taken

☝️ 1
1
lassemaatta08:06:38

the state of an atom is a Java AtomicReference

1
Jim Newton08:06:12

another question is for the case the value in the atom is a non-trivial value. how is equivalence determined? is it java which is determining equivalence? or is it clojure? if it is java then it does not know about the clojure = function. right?

Jim Newton08:06:41

and what does Java == do? explained to a java-null.

exitsandman08:06:58

reference equality, i.e. two references are == if they point to the same address in memory

1
Jim Newton08:06:59

if it is reference equality, then I’m free to put a huge immutable data structure in the atom. right?

lassemaatta08:06:06

I haven't written a lot of atom-heavy programs, but my feeling is that while using atoms is generally simple (and dare I say even easy), some care has to be taken when dealing with a lot of contention between threads. That is, if you have a huge datastructure and multiple threads trying to manipulate different parts of it; you might end up wasting a lot of cpu time recalculating stuff. And then you start thinking about splitting the state, but that has it's own downsides of course.

lassemaatta08:06:21

but yes, an atom can reference a huge data structure, as it's technically just an object reference to somewhere

Jim Newton08:06:50

one application in my research is BDDs (binary decision diagrams). And an area of current research is how to handle these in multiple threads. They are exponentially large data structures with lots of sharing between them. However, the equivalence predicate is atomic. I’m wondering whether there is something interesting I can do with BDDs which the clojure language makes easy, but is difficult in other languages.

Jim Newton08:06:37

its a solution looking for a problem.

exitsandman08:06:44

I never profiled so take this with a grain of salt, but I reckon that the overhead to the CAS isn't particularly significant vs the expected time spent operating on the data you'd put in the atom. As @U0178V2SLAY says, the main performance concern is waste of computation under heavy contention.

lassemaatta08:06:25

regarding the BDDs, I'd vager that clojures persistent data structures might be the major difference between clojure and other langs and perhaps those might offer some benefits with BDDs. Atoms themselves (as far as I can tell) are a thin wrapper around Javas atomic primitives, so I'd guess there isn't much difference there.

lassemaatta08:06:31

of course there's also STM (https://clojure.org/reference/refs), which I think is novel and perhaps that's something that might interest you?

growthesque17:06:25

is there something in clojure that's genereally considered best avoided like var in javascript?

didibus17:06:19

Nothing that jumps immediately too mind

growthesque17:06:11

i've heard someone saying that you should avoid macros at all cost or something.

hiredman17:06:09

The advice is to generally prefer data, and then use functions, and then use macros

3
growthesque17:06:17

how can you prefer data over functions, isn't data static?

hiredman17:06:23

Data is more manipulatible than a function is, the only thing you can do with a function is call it

didibus17:06:17

It's not data on its own. It's data that gets interpreted into behavior.

didibus17:06:31

For example: [:add :user 124 "John" ""] This is data. The same as a function is: (add-user 124 "John" "")

exitsandman17:06:32

macros aren't to be avoided at all costs, some things can only be done reasonably with them

exitsandman17:06:46

speaking of things related to macros and to avoid at all costs, clojure.walk/macroexpand-all

didibus17:06:36

The data describes the behavior, while the function executed immediately. The data is better, because it can be more easily inspected, recorded, replayed, batched, etc. You can then interpret the data and apply corresponding behavior. The behavior you apply is easy to change and all that, without breaking the data representation, etc.

exitsandman18:06:18

Basically, it completely disregards special forms and the context parameters &env and &form For instance:

user=> (walk/macroexpand-all '(quote (cond-> x y z)))
(quote (let* [G__142 x] (if y (z G__142) G__142))) 

Ed20:06:40

People tend not to use structs anymore. I think records mostly replaced them, though they still work.

tomd09:06:30

flatten - rarely the right thing. Usually rejected in favour of flattening to a certain depth using (sequence cat…) or mapcat with a relevant function • pmap - also a big source of unexpected behaviour when trying to parallelize owing to its semi-laziness and use of futures. can be used effectively, but folks usually prefer more easily customizable alternatives like claypoole • read and read-string on untrusted sources - are security vulnerabilities, as they can execute arbitrary code. clojure.edn alternatives are usually preferred • with-local-vars - probably the closest thing to var in js. I’ve never seen this used in the wild. It’s mostly unknown, and I think most people around here would be happy for it to stay that way • most of clojure.core.reducers - since transducers came to clojure, the reducer versions of map, filter, etc. are effectively deprecated. fold does bring functionality that doesn’t exist in core, but equally people may prefer a dedicated 3rd party library • memfn - hasn’t ever been commonly used (as the reader-literal fn form is just as good), but especially with Clojure 1.12 Method Values, it will become effectively deprecated • There are also a few functions in core that are actually marked as deprecated (such as replicate)

4
🙏 3
jumar09:06:23

A great list. It reminded me Clojure Don’ts https://stuartsierra.com/tag/dos-and-donts

🙏 1
tomd10:06:43

🙏 ah that’s a really good (if slightly more opinionated!) resource

growthesque10:06:02

thanks, great info

growthesque13:06:48

this seems horrible from my noob perspective: https://clojuredocs.org/clojure.core/with-redefs

tomd14:06:58

Yeah you rarely ever see it used in production code. Some people like to use it for mocking in tests. Monkey patching can lead to neater function signatures, but a lot of folks would prefer to take the dependency injection path. You can certainly write very good Clojure without ever reaching for it

didibus04:06:38

Those are all good call outs. None of those are terrible, but some definitely are either have newer approaches that are often better or can be tricky and have caveats to be careful about. Which probably means you should look for an alternative first.