This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2022-12-23
Channels
- # adventofcode (13)
- # announcements (7)
- # atom-editor (2)
- # babashka (6)
- # beginners (77)
- # biff (2)
- # calva (14)
- # cider (25)
- # circleci (2)
- # clj-on-windows (39)
- # clojars (1)
- # clojure (36)
- # clojure-belgium (4)
- # clojure-europe (78)
- # clojure-norway (25)
- # clojure-spec (1)
- # clojurescript (11)
- # clr (1)
- # cursive (1)
- # datahike (43)
- # datomic (6)
- # dev-tooling (3)
- # emacs (5)
- # exercism (1)
- # jobs (1)
- # jobs-discuss (3)
- # kaocha (2)
- # lsp (32)
- # malli (4)
- # music (1)
- # off-topic (14)
- # pathom (4)
- # reitit (14)
- # shadow-cljs (5)
- # tools-deps (3)
- # vim (1)
- # xtdb (5)
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)
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
OK whereas in bash for example I can do:
mynumber=6
((++ mynumber))
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?
I'm just trying to understand whether immutable data structures actually makes any difference or if it's just an implementation detail
I mean I can use conj to add an extra item to a vector:
(let [mynumbers [34 76]
mynumbers (conj mynumbers 81)]
mynumbers)
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.Let me put it this way. If someone didn't know Clojure had immutable data structures, how could they tell?
The previous example should puzzle them. Or something like this:
(def a 1)
(inc a) ;=> 2
a ;=> 1
OK so it just means we can't modify "in-place" without specifying a binding
then why not forbid re-binding variables?
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
Rebinding the same name is considered poor style but because the semantics are well-defined, it is not prohibited by the language itself.
(let [a 1
b (inc a)
a (inc b)]
[a b])
This is valid code but probably ought to have better names 🙂@U04V70XH6 Does lisp let*
allow rebinding?
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])))
right OK so I can imagine that every binding in a let is a nested let inside the previous
(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)
(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. Var
s are mutable references to immutable data.yeah so you can destroy the language completely with a few defs:
(def let 5)
(def def 7)
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.But also:
user=> (let [if 42] [if])
[42]
OK but after I do (def def 7)
(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)
user=> (def def 1)
#'user/def
user=> (def x 2)
#'user/x
user=> x
2
user=>
right I see yes
So def
is still treated as "built-in def
" in the "function position".
but I'm guessing this isn't best practice to use def as variable name XD
But if I think reference def
in a different context:
user=> (def y def)
#'user/y
user=> y
1
Definitely not "best practice" no 🙂
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.
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)] ...)
see I shadowed redirect at the bottom
I don't like nested destructuring so I wouldn't write that in the first place 🙂
But I would probably have redirect' (urlencode redirect)
I assume the apostrophe there is a typo? But yes if not doing destructing it does solve the problem
foo'
is a valid symbol. The '
is just part of the name.
I have a math background so '
reads as prime
to me: foo'
is foo prime
fascinating
'foo
is a symbol literal the same as (symbol "foo")
and 'foo'
is also a symbol literal the same as (symbol "foo'")
user=> 'foo'bar'
foo'bar'
yes that could confuse a stupid person
I'm tragically uneducated but hypothetically maybe I'll get around to learning maths one time
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.
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.
https://clojure.org/guides/weird_characters and https://clojure.org/reference/reader#_symbols are good sections to read about this.
(ok bed time here!)
thanks for your help (morning here)
goodnight
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.Generally "immutable data" structures refers to the the collections (vector, map, seq, set etc) being immutable (unlike other languages where they are mutable).
@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.
@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.Can anyone suggest good examples to study for wrapping a mutable thing (e.g. some type of Java InputStream) in a seemingly-immutable facade?
you could convert it to a sequence, but it’s hard to give advice without knowing more about your usecase
Clojure's data structures are one such example: https://youtu.be/toD45DtVCFM?t=1429
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)
I think as long as you transfer ownership of the mutable object, you can get pretty far
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
Yes that's true, if nobody else knows about the mutable thing, and there are no side effects
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))
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
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@U89SBUQ4T Wouldn't that realize the entire underlying input-stream at once, though?
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
no, because clojure data structures are lazy
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
and if you never do anything with it, it would call .read
0 times
So repeatedly
is lazy then?
I would expect
(repeatedly #(do (println "reading")
(.read istream)))
(take-while #(not= -1 %))
to realize istream
.(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
yes, repeatedly is lazy, it only calls the function if a new element is needed
here’s its (simplified) implementation
(defn repeatedly [f]
(lazy-seq (cons (f) (repeatedly f))))
lazy-seq
is a macro that makes this all work
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
cons
means this element followed by this remaining sequence