Fork me on GitHub
#beginners
<
2022-12-23
>
Joseph Graham06:12:47

Hi all. I'm sure this is dumb question but if Clojure uses immutable data structures why can I redefine variables? (let [mynumber "53" mynumber (Long/parseLong mynumber)] mynumber)

dpsutton06:12:38

you have shadowed mynumber with another local called mynumber. You cannot update either value. The first one is effectively gone as nothing can reference it. But it is not changed

Joseph Graham06:12:37

OK whereas in bash for example I can do:

mynumber=6
((++ mynumber))

dumrat06:12:59

AFAIK, let creates local bindings and whatever was last bound to a symbol is the value of it hence 53. What are you trying to achieve/emulate?

Joseph Graham06:12:38

I'm just trying to understand whether immutable data structures actually makes any difference or if it's just an implementation detail

Joseph Graham07:12:33

I mean I can use conj to add an extra item to a vector:

(let [mynumbers [34 76]
      mynumbers (conj mynumbers 81)]
  mynumbers)

dumrat07:12:37

Would be more helpful not to use the same binding here:

(let [mynumbers1 [34 76]
      mynumbers2 (conj mynumbers 81)]
  [mynumbers1 mynumbers2])
You will see that return is [[34 76] [34 76 81]] meaning that first value wasn't touched. Not sure if you are trying to explore something else though.

Joseph Graham07:12:42

Let me put it this way. If someone didn't know Clojure had immutable data structures, how could they tell?

dumrat07:12:24

The previous example should puzzle them. Or something like this:

(def a 1)
(inc a) ;=> 2
a ;=> 1

Joseph Graham07:12:50

OK so it just means we can't modify "in-place" without specifying a binding

Joseph Graham07:12:30

then why not forbid re-binding variables?

Joseph Graham07:12:49

I guess at this point I understand well-enough. Maybe I was never exposed to enough languages that do mutation so I don't understand the difference

seancorfield07:12:35

Rebinding the same name is considered poor style but because the semantics are well-defined, it is not prohibited by the language itself.

👍 1
seancorfield07:12:25

(let [a 1
      b (inc a)
      a (inc b)]
  [a b])
This is valid code but probably ought to have better names 🙂

👍 1
dumrat07:12:59

@U04V70XH6 Does lisp let* allow rebinding?

seancorfield07:12:39

The first a and the second a are different things -- the second one shadows the first one, as if it were a nested let:

(let [a 1]
  (let [b (inc a)]
    (let [a (inc b)]
      [a b])))

👍 1
1
Joseph Graham07:12:24

right OK so I can imagine that every binding in a let is a nested let inside the previous

seancorfield07:12:01

(and just to be clear: it is not a "dumb question" to ask stuff like this -- if you're not used to immutable data, these are obvious and common questions to ask)

seancorfield07:12:20

(def a 1)
(def b (inc a))
(def a (inc b))
is another scenario that folks ask about -- and in this case the "root binding" of a changes with that second def so although b gets bound to 2 (and that doesn't change), a is bound to 1 on the first line and then rebound to 3 on the third line. Vars are mutable references to immutable data.

Joseph Graham07:12:17

yeah so you can destroy the language completely with a few defs:

(def let 5)
(def def 7)

seancorfield07:12:09

You can't redefine special forms but you can redefine macros that overlay special forms. For example:

user=> (let [if 42]
         (if 13 :yes :no))
:yes
Because if is a special form.

seancorfield07:12:10

But also:

user=> (let [if 42] [if])
[42]

Joseph Graham07:12:56

OK but after I do (def def 7)

seancorfield07:12:08

(so if in a "function position" is still treated as the special form if even if there's a symbol if bound to a value)

seancorfield07:12:40

user=> (def def 1)
#'user/def
user=> (def x 2)
#'user/x
user=> x
2
user=>

Joseph Graham07:12:49

right I see yes

seancorfield07:12:07

So def is still treated as "built-in def" in the "function position".

Joseph Graham07:12:27

but I'm guessing this isn't best practice to use def as variable name XD

seancorfield07:12:37

But if I think reference def in a different context:

user=> (def y def)
#'user/y
user=> y
1

seancorfield07:12:47

Definitely not "best practice" no 🙂

seancorfield07:12:00

Shadowing any symbol is considered poor practice -- whether it's an argument, a local binding, a global Var, or a clojure.core symbol -- although there are exceptions to the latter, which is why (:refer-clojure :exclude [..]) exists in the ns form.

Joseph Graham07:12:47

OK so how would you re-write this? (real example)

(let [{{image_id "id"
          redirect "redirect"} :query-params
         {newtag :tag
          whichform :whichform
          newcaption :caption} :params} request
        redirect (urlencode redirect)] ...)

Joseph Graham07:12:04

see I shadowed redirect at the bottom

seancorfield07:12:32

I don't like nested destructuring so I wouldn't write that in the first place 🙂

seancorfield07:12:58

But I would probably have redirect' (urlencode redirect)

Joseph Graham07:12:58

I assume the apostrophe there is a typo? But yes if not doing destructing it does solve the problem

seancorfield07:12:17

foo' is a valid symbol. The ' is just part of the name.

seancorfield07:12:42

I have a math background so ' reads as prime to me: foo' is foo prime

seancorfield07:12:39

'foo is a symbol literal the same as (symbol "foo") and 'foo' is also a symbol literal the same as (symbol "foo'")

seancorfield07:12:11

user=> 'foo'bar'
foo'bar'

Joseph Graham07:12:21

yes that could confuse a stupid person

Joseph Graham07:12:26

I'm tragically uneducated but hypothetically maybe I'll get around to learning maths one time

seancorfield07:12:27

You will find code using ' at the end of symbol names (for prime style denotation of "subsequent" versions of a symbol) but hopefully you won't find ' embedded in a symbol name in real world code.

seancorfield07:12:54

I think the most surprising thing for folks learning Clojure is that a lot of characters are valid in symbol names... ? and ! are common, for example, and you'll see & and -> in the middle of symbol names.

😶 1
seancorfield07:12:02

(ok bed time here!)

Joseph Graham07:12:31

thanks for your help (morning here)

Rupert (All Street)10:12:53

I like this question. Whilst clojure data is immutable - it doesn't have to inconvience the developer. If you take a let statement and treat it as nested let statements and add printlns you get:

(let [a 1]
  (println "A:" a)
  (let [b (inc a)]
    (println "A:" a "B:" b)
    (let [a (inc b)]
      (println "A:" a "B:" b))
    (println "A:" a "B:" b))
  (println "A:" a))
Which outputs:
A: 1
A: 1 B: 2
A: 3 B: 2
A: 1 B: 2
A: 1
You can see that even though A is masked with a value of 3 it's never actually changed and is still 1 at the end.

Rupert (All Street)10:12:57

Generally "immutable data" structures refers to the the collections (vector, map, seq, set etc) being immutable (unlike other languages where they are mutable).

kennytilton15:12:20

@U043HLWSYUQ wrote: "so you can destroy the language completely with a few defs" With one. When our shop discovered Common Lisp, one of us destroyed it by changing the value of nil. I forget to what.

kennytilton15:12:53

@U043HLWSYUQ asked: "If someone didn't know Clojure had immutable data structures, how could they tell?" If they are coming from Common Lisp, they might notice right away. In CL, where mutation is allowed, a strong cultural, experience-based tradition leads devs to eschew mutation religiously, except in exceptional circumstances where mutation is both safe (from our understanding of the code) and necessary for efficiency or to make a change universally visible. But if they tried this they would catch on:

(defn config-for-debug [config]
    (assoc config :debug :on)
    config)
In CL, (setf gethash) is a so-called destructive operation, so a CLer might trip here.

skylize14:12:36

Can anyone suggest good examples to study for wrapping a mutable thing (e.g. some type of Java InputStream) in a seemingly-immutable facade?

Miķelis Vindavs14:12:07

you could convert it to a sequence, but it’s hard to give advice without knowing more about your usecase

Matthew Downey17:12:02

Clojure's data structures are one such example: https://youtu.be/toD45DtVCFM?t=1429

Matthew Downey17:12:01

There's no such thing as trivially wrapping something mutable in an immutable facade, unless you are willing to make the immutability a leaky abstraction (is it okay to put your thing in an atom and swap it? Etc)

Miķelis Vindavs17:12:40

I think as long as you transfer ownership of the mutable object, you can get pretty far

👍 1
Matthew Downey17:12:13

This is an example of using a syntax / calling convention that mirrors those used with immutable data structures, which has some advantages, but I wouldn't say it's a facade of immutability: https://github.com/stuartsierra/component

Matthew Downey17:12:04

Yes that's true, if nobody else knows about the mutable thing, and there are no side effects

Miķelis Vindavs18:12:37

Here’s a really contrived example of what I mean

(defn stream-seq [istream]
  (->> (repeatedly #(do (println "reading")
                        (.read istream)))
       (take-while #(not= -1 %))))

(->> (stream-seq (io/input-stream "resources/inputs/2022/day01.txt"))
     (take 5)
     (map char))

Miķelis Vindavs18:12:59

The thing returned by stream-seq is going to be a lazy sequnce that you can map, filter, etc. If it needs more chars it will call .read on the underlying stream. I put the println there to make it clear when it does that

Miķelis Vindavs18:12:20

And it can be reused, for example if you do

(let [s (stream-seq (io/input-stream "resources/inputs/2022/day01.txt"))]
  [(->> s (take 5) (map char))
   (->> s (take 5) (reduce +))])  
It will only print “reading” 5 times, and reuse the realized part of the sequence in the second usage

skylize18:12:43

@U89SBUQ4T Wouldn't that realize the entire underlying input-stream at once, though?

Miķelis Vindavs18:12:49

more general “unfolding” e.g. when you would not just call the same method again, but perhaps pass some params to it that depend on the result of the previous call could be implemented using lazy-seq For examples of that, the clojure.core NS is a good place to look. for example the implementations of repeatedly or take-while

Miķelis Vindavs18:12:06

no, because clojure data structures are lazy

Miķelis Vindavs18:12:35

meaning, if you take 5 it doesn’t run through the whole sequence, it just returns a new sequence that returns up to 5 elements. . when needed

Miķelis Vindavs18:12:46

and if you never do anything with it, it would call .read 0 times

skylize18:12:59

So repeatedly is lazy then? I would expect

(repeatedly #(do (println "reading")
                        (.read istream)))
       (take-while #(not= -1 %))
to realize istream.

Miķelis Vindavs18:12:37

(def s (stream-seq (io/input-stream "resources/inputs/2022/day01.txt")))
=> #'s

(first s)
reading
=> 49

(first s)
=> 49 ;; the first element was already read, so we don't have to "get" it again

(nth s 5)
reading
reading
reading
reading
reading
=> 10

(nth s 2)
=> 57

(nth s 3)
=> 57

Miķelis Vindavs18:12:59

yes, repeatedly is lazy, it only calls the function if a new element is needed

skylize18:12:38

👍 Sweet. Then that's pretty much exactly what I was looking for.

Miķelis Vindavs18:12:45

here’s its (simplified) implementation

(defn repeatedly [f]
  (lazy-seq (cons (f) (repeatedly f))))
  

Miķelis Vindavs18:12:56

lazy-seq is a macro that makes this all work

Miķelis Vindavs18:12:52

it returns a sequence object , that has a head and a tail when first is called on it will check if it already has a realized head. if it does, it will return that cached value, if it doesn’t, it calls the f and stores the result

Miķelis Vindavs18:12:19

cons means this element followed by this remaining sequence