Fork me on GitHub
#clojure
<
2021-01-08
>
didibus05:01:32

How do you unbind a binding? Is that even possible? Say I want the var deref to fetch the root again, but I'm inside a (binding [] ...) expression?

euccastro06:01:21

@didibus maybe there's a better way, but (alter-var-root #'*foo* identity) works

didibus06:01:10

That also made me think to look at the implementation, seems you can also do:

(.getRawRoot #'my-var)

👍 3
euccastro06:01:35

nice, thanks!

noisesmith07:01:39

alter-var-root doesn't change the binding inside a binding context though

noisesmith07:01:12

oh - but it returns the identity, I see

noisesmith07:01:46

it doesn't do the thing literally asked for - unbinding

didibus07:01:45

think I said "or get the root binding"

didibus07:01:00

Anyways, I just wanted a way to get the root value even in there is a thread binding

didibus07:01:44

I guess there is popThreadBinding too or something like that... not sure if that would unbind?

didibus07:01:27

Except with that, you can't really choose which binding to remove it seems

noisesmith07:01:28

that just crashes the repl if you didn't have a matching push-thread-bindings

noisesmith07:01:40

it's low level and fragile

noisesmith07:01:54

in fact binding is just a macro around push-thread-bindings / pop-thread-bindings

didibus07:01:38

If its a stack though, there's probably no unbind function

dpsutton10:01:23

The end of the binding context will have that pop thread bindings.

noisesmith17:01:39

right, and popping twice, or failing to pop, will break things

didibus18:01:25

Ya, but also, if you want to remove a binding which isn't the last, you can't because it's a stack, so I wouldn't think there'd be a function for it

didibus18:01:16

Though it makes me curious about the lookup process, I would have expected having a map of stacks or something, now it seems you have to search the stack for the newest instance.kf the specific var

noisesmith18:01:10

it's a stack of maps - each layer is a hash map of shadowings of the ones below it

didibus18:01:41

Doesn't that mean it needs to traverse through the whole stack? Imagine:

(def ^:dynamic foo 1)

(binding [foo 2]
  (binding [other :bar]
    (binding [other :baz]
      foo)))

didibus18:01:52

And I'm guessing to find that there are no binding it also must traverse through it all:

(def ^:dynamic foo 1)

(binding [other :bla]
  (binding [other :bar]
    (binding [other :baz]
      foo)))

noisesmith18:01:08

that's what it seems like, yeah

didibus18:01:44

And the structure would be like:

[{:other :baz} {:other :bar} {'other :bla} {'foo 1}]

didibus18:01:03

That might explain why dynamic vars are slower than ThreadLocal

noisesmith18:01:07

well vectors as stacks go in the opposite direction, but yeah

didibus18:01:28

AH lol, I wasn't sure and actually typed it the other then flipped it 😛

didibus18:01:52

I would have expected it to be:

{'foo [1 2]
 'other [:bla :bar :baz]}

didibus08:01:55

(conj)
;=> []
(conj nil)
;=> nil
(conj nil 1)
;=> (1)
Kinda weird how inconsistent conj is here no?

vlaaad08:01:41

Knowing about 0 and 1 arities involvement in reducers/transducers, all I see is consistency, damn curse of knowledge!

cursork10:01:03

How to tell you've written too much clojure? The above behaviour makes perfect sense to me

caumond10:01:33

Could you comment the first one, I don't get it

caumond10:01:48

why is it consistant to return an empty vector?

thumbnail11:01:55

0 arity is often used to produce an initial value

6
👍 3
pinkfrog13:01:09

why (conj) doesn’t produce () ? since (conj nil 1) gives (1). Also, why (conj nil) gives nil ?

vlaaad13:01:02

1-arity is completing arity for transducers, e.g. something that does final post-processing. Usually it behaves as identity, but some transducers like partition-all can add accumulated elements to a final result.

vlaaad13:01:54

(conj) returns vector instead of list because vectors are more useful and performant data structure than linked lists

vlaaad13:01:16

why should it return ()?

vlaaad13:01:53

(conj [] 1) is [1] , not (1) after all

didibus17:01:55

I find it inconsistent that (conj) and (conj nil 1) don't both create a vector or a list personally

didibus17:01:13

And preferably it be vector for both, since like you said, more useful

cursork19:01:07

Re 0-arity. Look at (+) or (*). They return a 'reasonable' initial value to be used for repeated application of the same function. You agree vector is fine. Why does (+) return a long, when (+ 0M) returns a BigDecimal? Many of the 1-arity functions are essentially identity. Note that (+ nil) returns nil even though (+ nil 1) will error. Ditto with (conj :foo) vs (conj :foo 1) So why doesn't (conj nil :x) promote a nil to a vector? I'm not aware of any other fns where nil ever becomes anything other than (). So nil => () makes sense, as it's consistent with all other functions in core where nil can be used in place of a collection.

cursork19:01:33

nils == empty seqs all over the place, unless there's a good reason to diverge. I'd say for (conj) that if you actually need it, it's likely you want append-to-the-end as it's the common case, so a vector is used. But for (conj nil) that's being very explicit (in my mind) that you want to append to the front.

didibus21:01:46

Ok, so if I recap: (conj nil 1) returns an empty seq, because nil and empty seq are kinda treated the same sometimes. So conjoining on nil is like asking to conjoin on empty seq. And then (conj nil) returns nil because conj is a transducer and the 1-ary for transducer for conj has to be identity. But why does (conj) return vector? It's an okay default, but so would have been return (), and that would be more consistent it seems. Are there any reason for vector instead? Is it also due to transducers? Or is there a common idiom which make this a better return?

cursork22:01:23

(conj) with zero arity was added the same time as transducers but it's not a transducer itself. But given that context: I think it's about expectations; what would you expect (transduce (map inc) conj '(1 2 3)) to return? I'd expect some sequence of 2 3 4 (i.e. the same ordering, so a vec is the only choice no-arity). If you do (transduce (map inc) conj () '(1 2 3)) (or with a nil) you'll get 4 3 2 But 🤷 I'm just guessing. It seems all fairly reasonable to me when I come to writing code.

vlaaad22:01:41

> Are there any reason for vector instead? I think this is the wrong question to ask. You provide examples with nil: (conj) (conj nil) (conj nil 1) . Imagine the reverse situation if we had (conj) return () , and you thought about these examples:

(conj) => ()
(conj []) => []
(conj [] 1) => [1]
It would be fair to ask why not []?

vlaaad22:01:56

or why not #{} since conj preverses the type of coll on set as well: (conj #{}) => #{}, (conj #{} 1) => #{1}

vlaaad22:01:03

zero-arity conj is for creating a default value for use in transducing/reducing context, and vector is a good default in this case.

didibus05:01:17

If (conj) returned () then it be consistent. In the absence of a collection, you get a (). It would be consistent with (conj nil 1) at least, and the 1-ary is well a special case that has no choice but to be identity because of transducers.

didibus05:01:39

With regard to vector being more useful, yes I agree, and actually that's how I got here. I was doing:

(update m :a conj :b)
Where the key :a doesn't exist the first time around, and that's how I realized damn (conj nil :a) returns a list 😞 And I almost never want a list, and always a vector, so in that sense you could say its great that (conj) returns vector, since they are more useful. But now that also creates an inconsistency, which is a little bit of a gotcha which you need to be aware and remember carefully about that depending on the arity, when not given a collection, conj will not always default to the same type of coll.

didibus05:01:26

But, I'd love to learn that, actually (conj) returning a vec and (conj nil :a) returning a list turns out they allow for a ton of cool useful idiom scenarios, and its just my particular one isn't one of them. If that was true that would convince me, otherwise I think it might just be a case of slightly accidental accretion.

didibus05:01:03

Not complaining, more curious to learn some new idioms that maybe I don't know about. If there isn't and conj is just that way just as a kind of accretion artifact, that's fine, and lucky for me fnil saves the day in my case: (update m :a (fnil conj []) :b)

Alex Miller (Clojure team)15:01:24

hello all, just a reminder that clojure 1.10.2-rc2 was released this week - would love to have you try it and report back here if things look good, or if you see any issues. changelog can be found here: https://github.com/clojure/clojure/blob/master/changes.md#changes-to-clojure-in-version-1102. If you do try it please hit the check mark emoji here!

24
dharrigan14:01:59

been running 1.10.2-rc2 for the past few days on 3 backend services. all working normally 🙂 nothing weird observed 🙂

👍 4
seancorfield18:01:25

We have it running in QA (as of yesterday afternoon) and everything looks good so far. I expect we'll put it in production early next week (we're mostly on Alpha 4 in production right now, with one app running RC 1).

waffletower19:01:30

Running into an interesting issue with a deps.edn based project. I have often used Java's Thread/setDefaultUncaughtExceptionHandler in Clojure projects. Its use seems to require that the caller be a first class java object

waffletower19:01:35

Nice, that may make the tooling easier

noisesmith19:01:58

all clojure values other than nil are first class java objects - I think what you mean here is that it needs to implement a specific interface?

dpsutton19:01:27

(Thread/setDefaultUncaughtExceptionHandler
  (reify Thread$UncaughtExceptionHandler
    (uncaughtException [_ thread ex]
      (prn "ouch i died in a thread"))))
nil
user=> (.start (Thread. (fn [] (/ 1 0))))
nil
user=> "ouch i died in a thread"

waffletower19:01:33

Ya you are right Justin. I am not sure what the dependency is.

noisesmith19:01:40

(to me the distinction would be eg. Scala where many constructs that exist as far as the language is concerned don't exist in a first class way in the vm - clojure intentionally doesn't do that)

waffletower19:01:57

Oops, no I am using reify already. I even work at reify 😀

waffletower19:01:12

(defn set-exception-handler
  []
  (Thread/setDefaultUncaughtExceptionHandler
   (reify Thread$UncaughtExceptionHandler
     (uncaughtException [_ thread e]
       (do
         (timbre/errorf "top level exception handler caught exception on thread: %s" (.getName thread))
         (timbre/error e)

didibus21:01:29

What's your problem exactly?

waffletower19:01:55

which is easily obtained through ns and :gen-class

waffletower19:01:46

In a clojure cli deps.edn project, I am not able to successfully register an exception handler without AOT compiling the class that invokes it

clojure -e "(compile 'indignant.core)"
java -cp $(clojure -Spath):classes indignant.core

waffletower19:01:26

Trying to understand what might cause that requirement

borkdude19:01:46

Make a minimal repro, else it's hard for anyone to help you probably

👍 6
waffletower19:01:03

indeed thanks

waffletower19:01:24

might find the problem myself that way

noisesmith19:01:16

well that java -cp .. line, since it ends in indignant.core only works if you have created indignant.core as a class java can find

noisesmith19:01:43

have you tried java -cp $(clojure -Spath) clojure.main -m indignant.core ?

waffletower19:01:47

Actually this is useful, as it is another invocation where the exception handler fails to register. It does not reference the precompiled indignant.core class file.

waffletower19:01:05

that is the working example

waffletower19:01:23

I should try to make a small repro example

hiredman20:01:16

Thread/setDefaultUncaughtExceptionHandler is extremely brittle

hiredman20:01:11

in this case my guess would be the difference you are seeing is between the way the clojure runtime loads and runs a main function with -m argument vs. how java loads and runs a main class

hiredman20:01:53

running a main class happens more directly, so there is very little code between you and the jvm, so any exceptions that happen bubble up and are "uncaught"

hiredman20:01:53

with -m clojure's error reporting, added somewhat recently, will catch exceptions and generate a report on them, usually in /tmp somewhere

hiredman20:01:55

The defaultuncaughtexceptionhandler is something you can set, for just in case, but you can never assume it will ever get called

hiredman20:01:14

It is the finallizer of exception handling

kosengan20:01:59

A good read: Clojure: the Lisp that wants to spread *"*Clojure is (slowly) eating the world" https://simongray.github.io/essays/spread.html

seancorfield21:01:45

For the future, #news-and-articles is a better place to post links to blog posts etc.

waffletower21:01:08

Ok I'll add a minimal repro in this thread for the issue that I'd like to understand: successful use of Thread/setDefaultUncaughtExceptionHandler seems to require AOT compilation

waffletower21:01:04

won't register the handler

clj -m uncaught.core

waffletower21:01:18

does register the handler

mkdir classes
clj -e "(compile 'uncaught.core)"
java -cp $(clojure -Spath):classes uncaught.core

hiredman21:01:31

they both register the handler

hiredman21:01:47

in the clj -m uncaught.core the exception is caught

waffletower21:01:58

not in my case

waffletower21:01:11

% clj -m uncaught.core
WARNING: When invoking clojure.main, use -M
Execution error (ArithmeticException) at uncaught.core/-main (core.clj:17).
Divide by zero

Full report at:
/var/folders/g5/1rcmn5_n4sn2scczzjtp8lyc0000gp/T/clojure-3856576569065612249.edn

waffletower21:01:15

% java -cp $(clojure -Spath):classes uncaught.core
exception handler caught exception on thread: main
#error {
 :cause Divide by zero
 :via
 [{:type java.lang.ArithmeticException
   :message Divide by zero
   :at [clojure.lang.Numbers divide Numbers.java 188]}]
 :trace
 [[clojure.lang.Numbers divide Numbers.java 188]
  [clojure.lang.Numbers divide Numbers.java 3901]
  [uncaught.core$_main invokeStatic core.clj 17]
  [uncaught.core$_main doInvoke core.clj 14]
  [clojure.lang.RestFn invoke RestFn.java 397]
  [clojure.lang.AFn applyToHelper AFn.java 152]
  [clojure.lang.RestFn applyTo RestFn.java 132]
  [uncaught.core main nil -1]]}

hiredman21:01:42

you can see it runs you code wrapped in a try/catch with reporting stuff in the catch

hiredman21:01:23

again, if you read what I said previously in the channel, I explain this

waffletower21:01:16

I see what you are trying to say now. It gets caught in clojure main before it hits the handler

hiredman21:01:28

and in general, any place where you don't control the execution context of your code (callbacks, etc) might defensively wrap the execution of your code in a try/catch which will stop your exception from bubbling up

hiredman21:01:49

so, to quote myself Thread/setDefaultUncaughtExceptionHandler is extremely brittle

waffletower21:01:16

I disagree, and that is probably why I didn't read what you said after very carefully.

waffletower21:01:46

But you have helped me to look to clojure.main and how it wraps the code execution

waffletower21:01:20

using a jar invocation will allow this brittleness you speak of to disappear.

hiredman21:01:51

it solves your specific case, but as I said, anytime you hand off your code to be run by someone else (callbacks, threadpools, etc) it is susceptible to this kind of thing

hiredman21:01:07

even if you aot compile in other cases you will still have this issue, and even with aot compiling you will still have this issue if you loading the aot compiled code via clojure.main instead of directly invoking its -main via java's main entry point stuff

NPException21:01:30

The crucial point is that any try-catch (or thread-group specific exception handlers) outside of your control can stop exceptions from reaching your default exception handler.

👍 3
hiredman21:01:44

if all you care about is the top level, you might want to look at https://docs.oracle.com/javase/7/docs/api/java/lang/Runtime.html#addShutdownHook(java.lang.Thread) instead, but that also has provisos and limitations

waffletower21:01:29

agreed. Being fairly new to clj deployments without jars I did not realize that the main thread was outside of my control given the implicit clojure.main try/catch. You folks helped me see that. Thanks!

hiredman21:01:07

it didn't use to, until early 2019 that try/catch wasn't there and the exception would bubble up

hiredman21:01:11

but beginners are constantly complaining about stacktraces being ugly, and there is a lot of hand wringing in some corners about beginners, so at some point a lot of work was done to try and make error reporting friendlier, and that is how that try/catch and the writing of the full stacktrace to a file in /tmp got introduced

didibus21:01:24

I would put an entry on http://ask.clojure.org about this personally. It could be considered a regression is some way.

didibus21:01:43

At the very least, its a breaking change

didibus21:01:35

You could argue it should re-throw, and not run System/exit 1

didibus21:01:43

Or you could argue that gen-class main https://github.com/clojure/clojure/blob/master/src/clj/clojure/genclass.clj#L448 should do a similar bootstrapping to clojure.main

waffletower21:01:58

I didn't experience it as a breaking change, personally, as I have until now universally used jar deployments. The difference in behavior was confounding at first, and now seeing the enclosing try/catch makes perfect sense.

didibus22:01:38

I still think it would be worthwhile to bring up. There have been changes in the past around consistency of the various ways Clojure bootstraps an app.

didibus22:01:00

Personally, if possible, I don't see why you wouldn't want each mechanism to allow for the same basic assumptions

didibus22:01:34

If you look at the gen-class main, its so different to the clojure.main

didibus22:01:57

It doesn't bind any dynamic var, it doesn't catch top level errors and report them, etc.

didibus22:01:28

I don't know, seems weird that an application would run with -main from clojure.main but not genclass -main

didibus22:01:09

I'm curious how the new -X exec-fn setups the context

didibus22:01:19

If that's also different from clojure.main and genclass main

hiredman21:01:28

@waffletower see my explanation above

hiredman21:01:37

it isn't that using Thread/setDefaultUncaughtExceptionHandler requires aot, it is that when you run via an aot'ed -main method invoked via the jvm vs running via clojure's namespace loading mechanism

hiredman21:01:12

in those cases your code is being run in different contexts

👍 3
Jack Arrington21:01:04

Why is ISeq not implemented for PersistentVector?

hiredman21:01:23

because a vector isn't a seq

hiredman21:01:58

a seq is a basically a kind of iterator, so a similar question to consider is "why does a java.util.ArrayList return an iterator when you call the iterator method on it, instead of implementing instead of implementing java.util.Iterator directly"

Jack Arrington22:01:47

PersistentQueue isn't a seq either, but it implements ISeq thinking-face

hiredman22:01:51

that is a mistake in the type hierarchy that can never be fixed 😕

👍 3
😢 3
hiredman22:01:58

actually, that is not true

hiredman22:01:09

user=> (doseq [i (supers clojure.lang.PersistentQueue)] (prn i))
clojure.lang.IPersistentCollection
java.io.Serializable
clojure.lang.Counted
clojure.lang.IMeta
clojure.lang.Obj
clojure.lang.IHashEq
clojure.lang.Seqable
clojure.lang.IPersistentList
java.lang.Iterable
java.util.Collection
clojure.lang.IObj
java.lang.Object
clojure.lang.IPersistentStack
clojure.lang.Sequential
nil
user=>

hiredman22:01:21

user=> (instance? clojure.lang.ISeq clojure.lang.PersistentQueue/EMPTY)
false
user=>

Jack Arrington22:01:03

Hm, weirdly there seems to be a divergence between Clojure and ClojureScript here. In CLJS, PersistentVector does implement ISeq: https://github.com/clojure/clojurescript/blob/946348da8eb705da23f465be29246d4f8b73d45f/src/main/cljs/cljs/core.cljs#L6295

hiredman22:01:35

as I mention every time this comes up, there are a ton of differences between cljs and clj

hiredman22:01:16

it is all different, you cannot assume they are the same anywhere

👍 6
seancorfield22:01:47

As a Brit married to an American, I can confirm that! Even after 21 years of marriage we both still manage to say things that has the other one going "Huh?"

seancorfield22:01:46

But my wife has picked up some Cockney rhyming slang over the years 🙂 and when she leaves me "honey do" notes, she just signs them T.

seancorfield22:01:01

T is for Trouble. Trouble and strife = wife.

didibus22:01:21

In ClojureScript ISeq is a protocol, not an interface, that's why I believe

didibus22:01:13

With protocols you don't have hierarchies, they are more like traits, so its fine to have PersistentVector extend the protocol to ISeq, but with interfaces it would cause weird type casting issues

hiredman22:01:21

On the jvm types are very restrictive, so the type hierarchy and how you arrange it has real consequences, and the relationships between the types tends to become part of the api. JS virtual machines are very different when it comes to types. So my guess is the hierarchy and relationships are carefully thought out in clojure, and in cljs it is just whatever is faster and mostly seems to be correctly

didibus22:01:37

PersistentVector is not an instance of ISeq in ClojureScript either. It just extends the protocol

didibus22:01:50

(instance? ISeq [])
;;=> false
(instance? ISeq #queue [])
;;=> false

didibus22:01:27

My guess is nothing is actually an instance of ISeq, because its a protocol, nothing inherits from its type

didibus22:01:24

Maybe more interesting is to look at seq? in Cljs

(seq? []) ; false
(seq? #queue []) ; true
(seq? '()) ; true

p-himik22:01:17

Huh. Why does the #queue literal exist in CLJS but not in CLJ?

didibus04:01:36

Rich never added it in Clojure I guess, but David added it in ClojureScript maybe.

borkdude22:01:16

What benefits do structs have over maps, if you can't use defrecord/deftype? I did a small test but accessing a key from a struct doesn't seem to be any faster than from a map - is that right?

borkdude22:01:19

user=> (defstruct Foo :foo :bar)
#'user/Foo
user=> (def s (struct Foo 1 2))
#'user/s
user=> (time (let [s s] (dotimes [i 1000000000] (:foo s))))
"Elapsed time: 7141.330632 msecs"
nil
user=> (def s {:foo 1})
#'user/s
user=> (time (let [s s] (dotimes [i 1000000000] (:foo s))))
"Elapsed time: 6302.803225 msecs"
nil

lukasz22:01:20

Are structs even used nowadays? I might be remembering it wrong, but they predate records

borkdude22:01:12

Yes, they are replaced by deftype and defrecord, but I was considering using them for implementing something which can only be known dynamically

borkdude22:01:22

E.g. they are still used in some CSV lib and I might have a similar use case for it.

borkdude22:01:36

But if there is no significant win, I'm not sure why I should even bother

seancorfield22:01:15

IIRC structs have the keys in a predictable order -- so using them for CSV stuff sort of makes sense

borkdude23:01:36

ah, using a bigger example gives the expected result.

user=> (def ks [:a :b :c :d :e :f :g :h :i :j :k :l :m :n :o :p])
#'user/ks
user=> (def Foo (apply create-struct Foo ks))
#'user/Foo
user=> (def s (struct Foo 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6))
#'user/s
user=> (time (let [s s] (dotimes [i 1000000000] (:a s))))
"Elapsed time: 10425.754936 msecs"
nil
user=> (def s (zipmap ks (range)))
#'user/s
user=> (time (let [s s] (dotimes [i 1000000000] (:a s))))
"Elapsed time: 14030.239471 msecs"
nil
since smaller maps are backed by arrays

didibus05:01:42

(def get-a (accessor Foo :a))
(time (let [s s] (dotimes [i 1000000000] (get-a s))))
"Elapsed time: 2013.2259 msecs"

hiredman23:01:49

you could use functions as tuples

hiredman23:01:51

you know, maybe the worst of all three options

borkdude23:01:02

I mean: I didn't understand where you are going with this, but I'm curious ;)

hiredman23:01:04

user=> (def ks [:a :b :c :d :e])
#'user/ks
user=> (def t (let [[a b c d e] ks] (fn [f] (f a b c d e))))
#'user/t
user=> (time (let [t t] (dotimes [i 1000000000] (t (fn [a & _] a)))))
"Elapsed time: 11304.311272 msecs"
nil
user=> 

hiredman23:01:50

I was thinking about it because closed over values become fields in fns, which seems like it might be fast to access, but there are other issues with it that make it not great

hiredman23:01:25

1. it is not great because it is indexed access instead of by name 2. to do it really generically you have to use apply, which of course is not fast 3. in all likely hood you'll end up closing over a collection of values instead of individual values, so you get an extra hop

hiredman23:01:54

https://crypto.stanford.edu/~blynn/compiler/scott.html is a neat series building a mini haskell compiler that compiles down to lambda calculus (all data types are functions) and then from lambdas to ski combinators

👀 3
borkdude23:01:33

thanks for sharing :)