Fork me on GitHub
#clojure
<
2023-03-02
>
borkdude15:03:24

I guess this thing has been done many times before right? An unnested let-like macro, which lets you define vars step by step in a REPL, like in a test scenario, but expands into a nested let so it doesn't actually define a bunch of global vars?

isak16:03:33

Haven't seen it, cool idea

Sam Ritchie16:03:25

Scheme has this idea so this is nice for folks coming from that world for sure!

🎉 2
borkdude16:03:27

@U017QJZ9M7W can you link to any of the scheme stuff that does this?

Sam Ritchie16:03:03

I can find more examples from scmutils but the first answer here is the sort of thing I’ve seen a lot: https://stackoverflow.com/questions/56568358/racket-scheme-nested-define/56568468

Sam Ritchie16:03:09

(define (func n)
  (define (n1! f)
    (if (= f 0) 1
        (* f (n1! (- f 1)))))
  (define (iter i res)
    (if (= i n) (+ 1 res)
       (iter (+ i 1) (/ i (n1! i)))))
  (iter n 0))

Sam Ritchie16:03:28

basically letfn , these don’t create globals

borkdude16:03:58

but if you execute each sub-expression in the REPL, it still defines a global, right?

borkdude16:03:25

so in scheme, define inside define (func ..) won't introduce globals?

2
borkdude16:03:48

that's cool behavior

Sam Ritchie16:03:27

once you’re inside that first define you’re all local, but yes if you go statement by statement you get global

borkdude16:03:51

that's exactly the goal (hence the deflet macro)

borkdude16:03:48

I also want it to behave well for nbb with respect to awaiting promises

Sam Ritchie16:03:29

would be nice to check for defn too in there

Sam Ritchie16:03:42

or is that handled already by macroexpansion? probably no since, duh, the form’s not expanded before it hits your macro…

borkdude16:03:39

what would be the use case for defn here?

borkdude16:03:58

I usually don't use letfn, but just let + fn

borkdude16:03:14

so that would be def + fn in this macro

Sam Ritchie16:03:46

for sure, I can just imagine someone assuming defn works if def does and writing (translation of the above)

(defn func [n]
  (deflet
    (defn n1! [f]
      (if (zero? f)
        1
        (* f (n1! (dec f)))))

    (defn iter [i res]
      (if (= i n)
        (inc res)
        (iter (inc i) (/ i (n1! i)))))

    (iter n 0)))

Sam Ritchie16:03:33

@U04V15CAJ but recommending scope-creep to someone’s macro is usually not a good idea so ignore my suggestion if it feels wrong!! a very predictable “only def is handled here” is nice too

Sam Ritchie16:03:25

other maybe-you-want-to-handle-vs-ignore-cases I can think of….

(deflet
  (def cake 1)

  (let [x 10]
    (def face x))

  (do
    (def z 100)))

borkdude16:03:56

I think I'll only handle "top level defs"

👍 2
borkdude16:03:43

The nbb promise variant:

🎉 2
borkdude17:03:06

finally no more translating between def and let forms in UI tests

genmeblog18:03:44

clojure.tools.analyser.jvm fails to analyze the following (def ^double PI 3.14) I believe this is proper primitive tagging, am I right?

genmeblog18:03:43

(clojure.tools.analyzer.jvm/analyze '(def ^double PI 3.14))

Execution error (ExceptionInfo) at clojure.tools.analyzer.passes.jvm.validate/eval16782$fn (validate.clj:189).
Wrong tag: clojure.core$double@4e0d541b in def: PI

ghadi18:03:25

Var contents are always boxed, never primitive

p-himik18:03:35

You can use ^{:tag 'double}. It won't affect any types but it will be used for method resolution in interop.

genmeblog18:03:57

I've been doing it wrong whole my life, lol...

p-himik18:03:46

^double is not wrong, but it won't work for vars specifically. You can still use it in other places.

genmeblog18:03:32

yeah, I know, I was pretty sure that it works also in def but it's resolved to a function

Alex Miller (Clojure team)19:03:57

Var meta is resolved, so you can’t use the primitive symbol hints there unless you quote, but as Ghadi said, it won’t actually be a primitive anyways. One other case is for true constants, using a (properly quoted) double or long hint AND ^:const can result in primitive inlining

genmeblog19:03:47

thanks @U064X3EF3, one more question about constants, do constant and primitive expression will be treated as constant, eg will (def ^{:tag 'double :const true} HALF_PI (/ PI 2.0)) be inlined or not?

ghadi19:03:49

If you put const, you don’t need the tag

genmeblog19:03:10

even with an expression like (/ Math/PI 2.0)?

ghadi19:03:49

Vars contain values, not expressions

genmeblog19:03:01

of course, I know that, I'm just not sure when and how knowledge about :const is applied.

genmeblog19:03:52

this discussion is really helpful, thanks all

ghadi19:03:46

A var marked const is replaced by its contents when it is used

👍 2
ghadi19:03:06

A reference to a var is replaced by…

👍 2
ag20:03:05

why is this happening?

(merge {} {:foo 'bar}) ;; => {:foo bar}
why is it removing the quote?

phronmophobic20:03:52

What does quote do?

phronmophobic20:03:30

what happens if you evaluate just 'bar?

ag20:03:13

what do you mean? nothing. it shouldn't be evaluated at all

ag20:03:57

If I want to merge two maps with some quoted forms in them, merge shouldn't be manipulating them

magnars20:03:06

The ' is an instruction to not evaluate the symbol. It's much like "\n" - you wouldn't expect the string to contain two characters after this.

magnars20:03:42

it has nothing to do with the merge

ag20:03:52

I guess I'm gonna have to do: (merge {} '{:foo 'bar})

Nate20:03:02

user=> 'bar
bar

phronmophobic20:03:33

or ''bar, but it's pretty unlikely that's what you want

phronmophobic20:03:38

> (= ''bar (list 'quote 'bar))
true

seancorfield20:03:42

user=> (-> (merge {} {:foo 'bar}) :foo (type))
clojure.lang.Symbol
'bar produces the symbol bar and that's what's in your result.

seancorfield20:03:33

user=> (into [] (comp cat (map (juxt identity type))) {:foo 'bar})
[[:foo clojure.lang.Keyword] [bar clojure.lang.Symbol]]
🙂

ag20:03:43

yah, I'm surprised I never stumbled on that before

ag20:03:49

I've been trying to merge some config data with another hardcoded config map that gets passed to another function that expects things quoted, and it was failing

respatialized20:03:42

For a problem like that you may want a tool like rewrite-clj that can preserve information that will be discarded by the reader

grav21:03:06

How come this doesn't work:

=> (realized? (repeat 'foo))
Execution error (ClassCastException) at user/eval1 (REPL:1).
class clojure.lang.Repeat cannot be cast to class clojure.lang.IPending

grav21:03:45

According to docs, repeat should return a lazy seq, and realized? should operate on a lazy seq:

user=> (doc repeat)
-------------------------
clojure.core/repeat
([x] [n x])
  Returns a lazy (infinite!, or length n if supplied) sequence of xs.
nil
user=> (doc realized?)
-------------------------
clojure.core/realized?
([x])
  Returns true if a value has been produced for a promise, delay, future or lazy sequence.
nil

grav21:03:20

If I do (realized? (take 1 (repeat 'foo))) it works.

grav21:03:27

I can even do

(defn my-repeat [x] (lazy-seq (cons x (my-repeat x))))
(realized? (my-repeat 'foo))
=> false

borkdude21:03:33

(repeat 'foo) can never be realized but (repeat 10 'foo) can, does the latter work?

borkdude21:03:51

no, also not

grav21:03:07

I'm puzzled why it throws?

borkdude21:03:26

not implemented I guess

borkdude21:03:37

maybe ask Alex

👍 2
grav21:03:42

Yeah ... seems like a bug.

hiredman21:03:00

repeat's docstring is just wrong

hiredman21:03:20

it doesn't return a lazy seq, it returns a clojure.lang.Repeat which doesn't implement IPending

borkdude21:03:38

it might have returned a lazy seq in the earlier days, maybe?

hiredman21:03:57

but also you really shouldn't use realized? on a lazy-seq either

hiredman21:03:08

because it is pretty worthless

pesterhazy21:03:46

Repeat only quacks like a lazy-seq

😅 2
duckie 2
borkdude21:03:27

ClojureScript 1.11.60
cljs.user=> (realized? (repeat 10 'foo))
false
cljs.user=> (realized? (repeat 10))
false

🤷 2
hiredman21:03:08

user=> (realized? (for [i (range 10)] i))
false
user=> (realized? (cons 1 (for [i (range 10)] i)))
Execution error (ClassCastException) at user/eval156 (REPL:1).
class clojure.lang.Cons cannot be cast to class clojure.lang.IPending (clojure.lang.Cons and clojure.lang.IPending are in unnamed module of loader 'app')
user=> (realized? (lazy-seq (cons 1 (for [i (range 10)] i))))
false
user=> (realized? (seq (lazy-seq (cons 1 (for [i (range 10)] i)))))
Execution error (ClassCastException) at user/eval190 (REPL:1).
class clojure.lang.Cons cannot be cast to class clojure.lang.IPending (clojure.lang.Cons and clojure.lang.IPending are in unnamed module of loader 'app')
user=> (realized? (seq [1 2 3]))
Execution error (ClassCastException) at user/eval208 (REPL:1).
class clojure.lang.PersistentVector$ChunkedSeq cannot be cast to class clojure.lang.IPending (clojure.lang.PersistentVector$ChunkedSeq and clojure.lang.IPending are in unnamed module of loader 'app')
user=>

borkdude21:03:39

repeat used to be a lazy seq so this seems to be a breaking change https://github.com/clojure/clojure/commit/ee69dc46fc81ab2cee12fe69cc14679ea222585f

hiredman21:03:58

user=> (realized? (lazy-seq (seq [1 2 3])))
false
user=> (realized? (rest (lazy-seq (seq [1 2 3]))))
Execution error (ClassCastException) at user/eval214 (REPL:1).
class clojure.lang.PersistentVector$ChunkedSeq cannot be cast to class clojure.lang.IPending (clojure.lang.PersistentVector$ChunkedSeq and clojure.lang.IPending are in unnamed module of loader 'app')
user=>

hiredman21:03:39

basically, realized? for a seq only sort of returns something useful if literally looking at a value constructed by a call to lazy-seq

hiredman21:03:50

but the rest of any lazy-seq value might not be constructed using lazy-seq (it only needs to be a seq)

hiredman21:03:47

and calling seq on someting constructed with lazy-seq is by contract going to return something not constructed by lazy-seq

hiredman21:03:48

IPending was originally a thing for delays and promises (maybe futures?), and then someone came along and tried it on a lazy-seq, and it didn't work, and said "hey, it should work on lazy-seqs" and some how they get their patch merged, despite the implementation of it for lazy-seqs being pretty useless

Alex Miller (Clojure team)21:03:45

> repeat used to be a lazy seq so this seems to be a breaking change repeat still returns a lazy seq (it's just not a LazySeq)

hiredman21:03:24

there is an existing ticket to make realized? return true for anything that doesn't implement IPending, which would make it return true for the seq returned by repeat (https://clojure.atlassian.net/browse/CLJ-1752)

hiredman21:03:12

https://clojure.atlassian.net/browse/CLJ-1751 about realized? and LongRange has some interesting commentary

Alex Miller (Clojure team)21:03:29

it was a long point of discussion

Alex Miller (Clojure team)21:03:54

if anything here, I would want realized? to say more nuanced things about when it can be used

Alex Miller (Clojure team)21:03:07

and what you should expect about it

hiredman21:03:26

"this is a trap, and it caught you"

Alex Miller (Clojure team)21:03:57

there are pretty much no cases where I would use realized? with a sequence

2
✔️ 2
borkdude21:03:22

I meant, breaking as in (realized? (repeat ..)) probably used to work but not anymore, but 🤷

Alex Miller (Clojure team)22:03:54

well that was 8 years ago. if you're the first to notice ...

Alex Miller (Clojure team)22:03:14

but I don't really consider it breaking given that realized? "doesn't work" on lots of stuff and it's about a property of the thing. I don't think repeat promises to return something that works with realized?

borkdude22:03:43

I didn't even notice it, it was grav and I don't really care given that I've never really used realized? on seqs either 😆

Mario Giampietri22:03:10

> repeat still returns a lazy seq (it's just not a LazySeq) Uh 😓 this hit me! 😅 So, to rephrase: • there are sequences lazy as, strictly speaking, plain clojure.lang.LazySeq; • others are lazy as (logically?) infinite, not clojure.lang.LazySeq. Correct? I'd have never attempted to call (realized? (repeat ...) but I wasn't aware of this nuance.

borkdude22:03:18

I went through the first 5 pages on http://grep.app and couldn't find realized? on lazy seqs, mostly future/promise stuff

borkdude22:03:24

@U017QU43430 The type differences are usually relevant when implementing a protocol on seqs like this one: https://github.com/borkdude/finitize/blob/master/src/finitize/core.cljc

Alex Miller (Clojure team)22:03:46

well you probably shouldn't do that anyways. that code is missing Cycle for example.

Alex Miller (Clojure team)22:03:13

well, I guess LongRange is finite

Alex Miller (Clojure team)22:03:20

but cycle can take an infinite seq and is then infinite

borkdude22:03:00

The idea of the library is a bit beside the point

Alex Miller (Clojure team)22:03:35

the best thing with sequences is to assume that you don't know anything about the size, finite/infinite, or how much of it is realized

Mario Giampietri22:03:36

@U04V15CAJ sure, in general I'm well aware of that; this particular case was a blind spot to me, I was taking for granted people intended type LazySeq when speaking generically of lazy sequences (nevertheless I find the doc string in repeat clear enough to avoid the confusion).

borkdude22:03:59

(that library never got further than 0.0.1-SNAPSHOT anyway and is now 4 years old 😆 )

Alex Miller (Clojure team)22:03:34

probably more efficient to implement something like that with the new partitionv in 1.12 too :)

🤯 2
Alex Miller (Clojure team)22:03:26

if you did (first (partitionv n coll)), you'd get a realized vector (built with into using transients and reduce). and the lazy next partition utilizes the new fast IDrop implementation for colls that support it under the hood (via nthrest).

👍 2
borkdude22:03:03

I used that lib in https://borkdude.github.io/re-find.web/ to limit the amount of things that were being compared to not freeze the browser, in e.g. (= (range) (range)). that was 4 years ago. maybe today I would find a different solution