Fork me on GitHub
#beginners
<
2021-06-30
>
Ory Band10:06:35

Hi. if is a special form which only evaluates a single condition branch. If that is the case, why an exception is thrown in the following scenario? That is, why isn't `(1)` a compile time exception as well? Both `(1)` and `(nil)` should throw an exception, or none at all

delaguardo10:06:28

because this is compile time exception

Ory Band10:06:44

why isn't (1) a compile time exception as well then?

Ory Band10:06:59

both (1) and (nil) should throw an exception, or none at all

Ory Band10:06:16

this is actually what i was asking sorry if i wasn't clear

Ory Band10:06:09

edited the question

delaguardo10:06:16

hm… which version of clojure? it works fine for me

delaguardo10:06:41

mine is 1.10.3

Ory Band10:06:55

that's what i meant: only (nil) throws an exception, not (1)

Ory Band10:06:10

replace (1) with (nil) and you get an exception

Ory Band10:06:29

but both statements are illegal

delaguardo10:06:47

and I think this is an obvious (and cheap) compile time check.

teodorlu10:06:26

I think "if does not evaulate both branches" is meant as a performance optimization, not as part of the semantic.

Ory Band10:06:07

i don't understand why it is implemented this way. it feels inconsistent

delaguardo10:06:27

kinda, but it will be some significant performance penalty if it will check other types as well

delaguardo10:06:24

and it is not possible to do in general for the case of if without evaluation

solf12:06:04

@U3X7174KS It’s definitely required and not just a performance optimisation. Even if you don’t do anything with the returned value, a branch could have side effects. That’s actually why if can’t be a simple function and must be a macro

teodorlu12:06:11

What's required? Not sure we disagree.

solf12:06:24

That it doesn’t evaluate both branches

teodorlu12:06:10

Didn't Ory just provide a case where it evaluates both branches?

teodorlu12:06:52

I guess "syntax error" might be considered different from evaluation.

delaguardo12:06:47

This error thrown during compile time. Nothing is evaluated yet )

👍 4
Fredrik12:06:17

As @U04V4KLKC is pointing out, the error happens in the compiler, at compile-time, before evaluation. if does indeed not evaluate both branches, as part of it's semantics, but this doesn't prevent the compiler from seeing the form (nil) .

👍 2
Fredrik12:06:23

Maybe a reason for this explicit check is to give a more informative error message than NullPointerException .

delaguardo12:06:08

more like prevent NullPointerException from happening in case it will be definitely thrown in runtime. And this can happen with significant delay

(defn foo [] (+ 1 (nil)))
this form will fail during load not when foo function is called

👍 2
Ory Band12:06:48

i still don't understand why (nil) is a special case that causes failure in the reader, and not just let it fail in the evaluator like every other illegal expression. i don't see the advantage in treating (nil) differently

2
Fredrik12:06:22

Maybe think of it as a null-pointer check?

Ory Band12:06:47

what is so special about this check compared to every other check?

Fredrik12:06:01

It doesn't ever make sense to call null as a function

Ory Band12:06:17

so does (1)

Ory Band12:06:27

or ("") or a lot of other cases

delaguardo12:06:51

yeah, but null check is cheap and others are costly

Fredrik12:06:57

True, but not such a clear case. After all, (:key m) is a valid function call. Maybe in the future integers will also be callable?

Ory Band12:06:30

> but not such a clear case i don't understand why this clear case is so imporatnt

delaguardo12:06:53

Probably because NullPointerException is not really nice way to tell developers about the problem.

delaguardo12:06:45

For example (1) is throwing ClassCastException with enough information

Fredrik12:06:00

If I understand correctly, anything that implements clojure.lang.IFn can be called as a function. But null cannot have methods.

Yehonathan Sharvit12:06:00

I encountered this "issue" when trying to write a macro that received something that might be a function:

(defmacro maybe-call [a]
  `(if (fn? ~a)
     (println "result of function call: " (~a))
     (println "not calling")))
maybe-call works fine when it receives a function or a number but not when it receives nil

Yehonathan Sharvit12:06:09

(maybe-call (constantly true))
(maybe-call 3)
(maybe-call nil)

Yehonathan Sharvit12:06:19

Syntax error (IllegalArgumentException) compiling at (*cider-scratch*:14:1).
Can't call nil, form: (nil)

Yehonathan Sharvit12:06:44

Isn't it a unexpected consequence of the nil call check?

Fredrik12:06:19

Yes, but what do you mean by unexpected?

delaguardo12:06:11

(if (fn? nil)
  (println "result of function call: " (nil))
  (println "not calling"))
it expand to that. So - yes, it is a consequence of nil call check

Yehonathan Sharvit12:06:29

It's unexpected in the sense that it's hard to predict. A similar code in a function instead of a macro works fine

Fredrik12:06:10

Ahh, I see. Yes, unexpected from programmer's point of view, but not from the compiler's (macroexpanded) point of view :)

Ory Band13:06:41

is there some official clojure authority we can ask about this?

Fredrik13:06:57

@U011QKW5RGF "but both statements are illegal", yes, but for two very different reasons. And only one of these reasons can be checked in the compiler.

Ory Band13:06:14

my point is that none of these should be checked by the compiler - it fails in unexpected scenarios such as the one Yehonatan demonstrated

Ory Band13:06:43

or, at least, i don't see why it was implemented this way. i guess there's a good reason which i don't understand yet

Fredrik13:06:22

One reason I see is that immediately after the null-check, the compiler calls if(op.equals(FN)) . So a nullpointer error will be thrown regardless in case op == null

dpsutton13:06:11

A good place to post this question would be on http://ask.clojure.org

dpsutton13:06:34

And I think a good way to look at this is to remember that there is a phase where Clojure must emit jvm bytecode. So it isn't evaluating anything, but it does have to emit bytecode with instructions on how to take both branches. And there's no good way to do that with (nil)

dpsutton13:06:07

And interpreter might not have any problems with this, and in fact, bb has no problems returning 1 from (if true 1 (nil)). But if you need to emit code that performs both branches and then only jump to one of them, there isn't a great way to do this

dpsutton13:06:44

I don't know enough jvm to know if (nil) would be invalid bytecode or if its just patently clear that it will fail at runtime

Fredrik13:06:07

@U011QKW5RGF do I understand correctly that your point is that the compiler should check neither cases, because it leads to unexpected behaviour?

Ory Band13:06:44

yes. it should fail/crash at the eval stage, not the read stage

Fredrik13:06:11

Can I ask why?

Ory Band13:06:15

more precisely, i'd like to know why (nil) is treated in a special manner here

Ory Band13:06:45

> Can I ask why? i don't understand what's so special about (nil) compared to (1) or ("") or any other scenario

Fredrik13:06:16

A soft (and maybe wrong) answer: Because null is not an object

Ory Band13:06:22

1. this is a java implementation detail being unintentionally (i guess) exposed in clojure 2. is null the only non-object in java?

Fredrik13:06:20

nil doesn't have a type, 1 and "" do.

Ory Band13:06:44

is there any other thing in java that doesn't have a type?

Ory Band13:06:54

i'm really asking, i'm not too familiar with java

Fredrik13:06:41

I'm sorry I didn't make clear, in my last post I was talking about Clojure types.

Ory Band13:06:17

ok, same question then. is nil the only non-type in clojure? why do i even have to think about this when writing clojure

Fredrik13:06:48

Clojure doesn't hide the fact that it's a hosted language

octahedrion13:06:13

"`nil`  doesn't have a type, 1 and "" do." but, (defprotocol T (t [this])) (extend-type nil T (t [this] 7)) (t nil) => 7

Ory Band13:06:32

somebody call rich already and have him settle this discussion lol

octahedrion13:06:57

actually pretty sure in Clojurescript one can extend-type nil IFn so that (nil) is valid

octahedrion14:06:13

not quite

cljs.user=> (extend-type nil IFn (-invoke ([this] 0)))
nil
cljs.user=> (nil)
Unexpected error (ExceptionInfo) compiling at (<cljs repl>:1).
Can't call nil at line 1 
cljs.user=> (-invoke nil)
0

kongeor14:06:47

@U0CKDHF4L that's interesting. Can you try this with numbers?

octahedrion14:06:11

yes:

(extend-type js/Number IFn (-invoke ([this op x] (op this x))))
#object[Function]
cljs.user=> (71 + 171)
242

kongeor14:06:55

cool, thanks! 🙂

octahedrion14:06:06

therefore:

cljs.user=> (extend-type js/Number IFn (-invoke ([this] this)))
(warning ignored) 
#object[Function]
cljs.user=> (1)
#object[Number 1]

Fredrik14:06:43

Cool! It looks like a difference is that in Clojure, clojure.lang.IFn is a Java interface, while in Clojurescript, IFn is a Clojure protocol.

Fredrik15:06:45

The point being that Clojure protocols can be implemented for any existing type, including nil, while Java interfaces seem implemantable only when using making new types, eg. using defrecord or reify .

noisesmith16:06:16

@U011QKW5RGF clojure and the JVM are flexible enough that 1 could implement clojure.lang.IFn by the time you execute the code, this isn't the case for nil I don't think sane code would ever do such a thing, but the fact is that it's possible

👍 2
noisesmith16:06:35

oh I see someone up thread already demonstrated

noisesmith17:06:59

> why do I have to think about this when writing clojure? as mentioned before I think, clojure is explicitly a hosted language, and embraces the details of its host, there are other languages (racket is quite similar and very well thought out) that do intend to abstract the host implementation away, but you'll find that the reason clojure is as popular as it is is that it explicitly embraces the VM (thus works well in integration with Java or JS projects)

Ory Band11:07:44

so to summarize, if i understand correctly, the reason that in my original message calling (nil) throws a read-time exception while (1) doesn't is because in java null is not an object? that is, this is an implementation detail of the host language we cannot go around by?

Fredrik13:07:43

A syntax error for (nil) is thrown because it's not possible to call nil as a function

Ory Band13:07:11

.. in java (right?)

Fredrik13:07:36

Yes, functions seem to be invoked by calling the invoke method on objects

Ory Band13:07:17

thanks for the thorough answer. side question: does this also occur in cljs?

Fredrik13:07:23

Yes, the cljs compiler doesn't allow it

Ory Band14:07:31

thanks fvr. much obliged 🎩

Fredrik14:07:41

You're welcome! Trying to read and understand parts of the Java source of Clojure seems approachable, but the caveat is that I've never programmed in Java, so I hope I've understood things correctly.

noisesmith15:07:11

> the reason that in my original message calling (nil) throws a read-time exception while (1) doesn't is because in java null is not an object? right - the runtime is such that (nearly?) anything other than nil could be extended or replaced between read time and execution, but not nil

titanroark15:06:40

is there a way to immediately return from a short-hand literal fn?

;; doesn't work
(map #(%) [1 2 3])

;; works
(map (fn [x] x) [1 2 3])

Fredrik15:06:12

You could try #(do %) (or just use identity )

titanroark15:06:00

ah thank you, identity is what I was looking for

😀 2
ghadi15:06:53

fyi there is no early return in clojure

👍 2
dgb2316:06:44

there is reduced

dgb2316:06:57

btw a little trick to see how reader macros expand is to quote them and wrap them into read-string like so: (read-string (str '#(%)))

dgb2316:06:13

it would expand like so: (fn* [%1] (%1))

dgb2316:06:58

which shows why you’d have a problem when mapping over a vector of numbers

ghadi16:06:19

(Reduced is not the same thing as early return)

hiredman16:06:13

user=> (read-string (str '#(%)))
(fn* [p1__2#] (p1__2#))
user=> '#(%)
(fn* [p1__6#] (p1__6#))
user=>

😄 2
mzavarella19:06:19

Quick question about stm/atoms Given something like the code below, I'd like the update-state! function to return the value of the parameter k. Whenever I try to return that key, the state atom defined on line 1 does not get updated. I'm pretty rusty here so I was wondering if anyone could help me out. Thanks!

(def state (atom {}))

(defn add-key! [state k v]
    (swap! state assoc-in [k] v))
    
(defn remove-key! [state k]
    (swap! state update-in [k] #(dissoc % doc-id)))

;; Updates the state but returns the whole atom
(defn update-state! [state k v]
    (dosync
    	(remove-key state k)
    	(add-key! state k v)))

;; These both return the value `k` but do not update the state
(defn update-state! [state k v]
    (dosync
    	(remove-key state k)
    	(add-key! state k v)
    	k))

(defn update-state! [state k v]
    (dosync
    	(remove-key state k)
    	(add-key! state k v))
    k)

manutter5119:06:17

I’m pretty sure you don’t really want dosync in there, and I’m definitely sure you don’t need remove-key, since you immediately assoc a new value with that same key. I’d say just change the name of add-key! to set-key! and get rid of dosync and remove-key! (unless you want to keep it as a stand-alone function to delete keys from the map entirely).

manutter5119:06:27

Also I agree with using assoc instead of assoc-in, given that you’re just updating the value for a single key in an un-nested map.

drewverlee21:06:22

What is doc Id? It might be irrelevant but it's not defined.

drewverlee21:06:52

I think instead of dosync your update fn should just do both operations.

drewverlee21:06:34

I recommended against aliasing core fns unless they do something novel and it's shared. E.g just do (swap! S assoc k v) Atoms are mulithreaded state management that retry the logic on conflict. Do you need that?

Fredrik21:06:04

Does this do what you want? ;; Updates the state, returns k (defn update-state! [state k v] (swap! state assoc k v)) (swap! state update k #(dissoc % doc-id))) k)

drewverlee21:06:24

If not then (assoc m k v) is sufficient. A context where you need an atom would be if you were running a web server and multiple http request handlers were trying to coordinate. Say to return a count of times across all browsers a button was clicked.

drewverlee21:06:29

@U024X3V2YN4 if op wants those actions to be one transaction (all of nothing) then they all need to be on one fn passed to swap!

mzavarella21:06:00

Hey guys, thanks for the feedback but I think the purpose of the question has been missed through a poor explanation of the problem by me. What I was looking for was dorun to force the swap! to happen without having to rely on returning the value of the swap! for evaluation. Thanks again for the feedback

Fredrik21:06:16

Would you like to explain a bit more what you want to accomplish? Calling dorun is used to force side effects out of lazy sequences

drewverlee21:06:34

Can you try running this code? ;; make an atomic list (def players (atom ())) ;; #'user/players ;; conjoin a keyword into that list (swap! players conj :player1) ;;=> (:player1) It updates the state and swap! Does return the current value of the state.

drewverlee21:06:01

Do run is, as fvr said, for lazy collections

noisesmith22:06:48

to be clear, the only laziness in clojure is when something like map / filter etc. return a lazyseq, assoc is never lazy

mzavarella19:07:28

Instead of providing a more concise/minimal example, I gave a completely different example thinking this was more to do with STM than lazy evaluation. In my implementation, I use add-key! within a call to map wherein that call to map would either be evaluated when it's returned or if I force it with dorun. What I actually need to do ( I think ) is use doseqinstead of map. Better example implementation below:

;; state, add-key!, remove-key! from above

(defn add-keys! [index k vs]
  (map (fn [[k v]] (add-key! index k v)) vs))

;; which should be, I think
(defn add-keys! [index k vs]
  (doseq [v vs]
    (add-key! index k v)))

noisesmith17:07:16

> that call to map would either be evaluated when it's returned or if I force it with dorun to be clear, returning something never realizes it, but it gives the caller a chance to realize it eventually if they want to

noisesmith17:07:18

you can also use run! which is exactly like the two-arg version of map, except it returns nil and is eager

noisesmith17:07:39

(ins)user=> (run! println "hello, world")
h
e
l
l
...
nil

mzavarella16:07:48

Thanks @U051SS2EU, I'll check out the behavior with run! as well!