Fork me on GitHub
#beginners
<
2021-11-17
>
Shmarobes10:11:25

Hi. The clojure reference https://clojure.org/reference/protocols that "defprotocol is dynamic, and does not require AOT compilation". I'd like to know what the most important implications of this are. Does this mean that protocols can be generated during runtime or something like that? If so, could you give a real-world example where this might be used? I'm somewhat new to the idea of compile time vs. runtime polymorphism, so a simpler (if possible) explanation would be better. Thanks in advance.

1
Ben Sless11:11:15

Compile time polymorphism - pick the method to dispatch to at compile time Run time polymorphism - pick it at run time Interfaces are also a limited example of run time polymorphism. The difference with protocols is that you can extend them to types which don't implement your interface as well. When protocol methods are invoked, there's a check if the target is an instance of an interface backing that protocol. If so, it dispatches to it. Otherwise, there's a lookup in an additional method table with can be extended

Shmarobes11:11:51

@U02CV2P4J6S It is relevant, yes, but still doesn't really answer my question. This is why I asked for a "simpler explanation", actually. I'm hoping for something a noob could understand, because I'm not too familiar with how compilers work.

Shmarobes11:11:29

@UK0810AQ2 This sheds some light on the issue. Your explanation of compile time polymorphism implies that it's possible only in statically typed languages, as you need to know the argument's type at compile time, am I correct? Also, by "interfaces" do you mean class interfaces? I think I've got a basic understanding about differences between the two types of polymorphism, but the question about what this fact changes still remains.

Shmarobes11:11:11

The fact being that "defprotocol is dynamic"

Shmarobes11:11:48

What would work differently, if it was static?

Shmarobes11:11:19

I think I got it. The functions that are part of a protocol dispatch at runtime, as Ben said, which means that they don't need to know the argument's type at compile time. I'm not sure if this is even possible in clojure, as the types are not declared explicitly, but I think this is what was meant by defprotocol being dynamic. Please correct me if I'm wrong.

Ben Sless11:11:33

The answer here would be "yes and"

Ben Sless11:11:48

By interfaces I indeed meant class interfaces

Ben Sless11:11:26

The case with them is similar, if you have 10 classes implementing an interface, you don't know at compile time which class instance it is, so you're calling the interface method

Ben Sless11:11:56

then at run time, you go to the class and tell it, "hey, I know you know how to handle this, so if you don't mind"

Ben Sless11:11:58

Protocols are similar but stronger, at that case, the target class doesn't even have to implement the interface. You check for that first, and if it does, you dispatch to it, but if not, there's an extra dispatch table which can be extended dynamically

Ben Sless11:11:20

By dynamically, it means that even at run time, you can extend-protocol to an existing class you haven't met before

Shmarobes11:11:54

I got it, thank you!

Shmarobes11:11:44

Could you give a short usage example of extending protocol at runtime?

Ben Sless11:11:06

But, in everything there's a price, the price for this sweet feature in protocols is that you have to keep the call-site dynamic

Ben Sless11:11:22

you can't fully compile it ahead of time

Shmarobes11:11:31

So it's slower?

Ben Sless11:11:52

Slightly, and the JVM can't fully JIT compile that call site

Ben Sless11:11:05

An example of dynamic extension:

(let [extend (fn [protocol this]
               ;; cljs: class clojure.lang.PersistentList cannot be cast to class clojure.lang.Named
               #?(:clj (let [s? (satisfies? Schema this)
                             is? (satisfies? IntoSchema this)]
                         (extend-protocol Schemas (class this)
                           (-schema? [_] s?)
                           (-into-schema? [_] is?)))))]
  (extend-protocol Schemas
    nil
    (-schema? [_] false)
    (-into-schema? [_] false)
    #?(:clj Object, :cljs default)
    (-schema? [this] #?(:clj (extend Schema this)) (satisfies? Schema this))
    (-into-schema? [this] #?(:clj (extend IntoSchema this)) (satisfies? IntoSchema this))))
Taken from an older version of malli

👍 1
Shmarobes11:11:28

Thanks for the great answers.

Ben Sless11:11:46

Glad I could help 🙂

🙌 1
sheluchin13:11:03

Is there some function to turn {:a 1 :b 2} to :a 1 :b 2?

delaguardo13:11:21

(reduce concat (seq {:x 1 :y 2}))

sb13:11:55

‘(apply concat ,,,)’ isnt good? Or is that different?

Mno14:11:04

(def m {:a 1 :b 2})

(interleave (keys m) (vals m))
;=> (:a 1 :b 2)

(flatten (seq m))
;=>(:a 1 :b 2)

👍 1
Mno14:11:31

And then apply wherever you need that? hard to give a good suggestion without knowing why you'd need it.

sheluchin14:11:50

Ah, I was looking to use a map for keyword args prior to https://clojure.org/news/2021/03/18/apis-serving-people-and-programs but I now realize I should just destructure in the called function instead of in the caller.

sheluchin14:11:54

Thanks!

🎉 1
sb14:11:49

@UGFL22X0Q Yes, that was my question. I wasn’t at computer. Equivalent or not? I checked now, same result. Give back a lazySeq (apply concat m) but is that correct? First I thought flatten+seq as you wrote.. but my question.. apply concat is correct way to do? or not? (same result)

Mno14:11:17

Dunno! Whatever seems more legible would be my recommendation. I don't think there'd be many cases where the performance would be critical for this kind of thing.

sb14:11:14

Ok, thanks!

Benjamin15:11:52

is there a common naming for a xform function for a transducer? And is this something you would do:

(let [xf (comp ...)]
  (defn process [...]
    (into [] xf ...)))

Ben Sless19:11:59

Why not def the transducer?

Ben Sless19:11:27

I usually call it does-something-xf

Ben Sless19:11:43

an informative name about what it does

Benjamin14:11:50

cool thanks

Colin P. Hill15:11:21

Is there any official documentation anywhere on the semantics of the #: dispatch for namespaced keywords? Seeing a lot of blog posts and tutorials and such, but was hoping to find something authoritative

Colin P. Hill16:11:51

I did find that document, but it doesn't seem to include the dispatch for namespaced keywords. Unless I'm missing something?

Darin Douglass16:11:22

look for Map namespace syntax

Colin P. Hill16:11:31

aha, thank you!

dpsutton16:11:39

Yeah that’s a sugar for maps not necessarily just namespaced keywords

Darin Douglass16:11:48

i, personally, disable it universally

Darin Douglass16:11:39

i find it more confusing and generally unnecessary. instead of looking just at the keyword to know what i’m dealing with i now also have to check to see if the whole map is namespaced. i find that extra bit of friction annoying, so i just don’t allow clojure to do it 😛

Benjamin16:11:59

so you mean it's easy but not simple

Darin Douglass16:11:03

(the biggest problem with that is disabling it is a bit hacky since, iirc, *print-namespaced-maps* doesn’t work with pprint’d (and thus cider) data, so i had to drop down to overwriting clojure.pprint/lift-ns)

andy.fingerhut16:11:23

I do not know if the Clojure core team is interested in changing the Clojure implementation for this issue, but a question on http://ask.clojure.org might start the ball rolling towards improvements.

Darin Douglass16:11:46

i was misremembering my exact reasoning for doing what i did. as @U11BV7MTK mentioned below it was an nrepl problem

dpsutton16:11:06

i think the arrow on that is backwards. CIDER doesn’t work well with some bound variables. it is CIDER’s bug not Clojure’s. And I believe it is due to nrepl not necessarily CIDER

Darin Douglass16:11:52

ah gotcha. i knew is was something in that ballpark. it’s been a bit since i set my stuff up

dpsutton16:11:53

some background. the issue is closed but seemingly not fixed

Andy Carlile17:11:33

I'm trying a different approach (different to my previous attempt) to getting sente and reagent in a new project: steps to take: 1. lein new reagent pokedex +cider 2. include sente and http-kit deps in project.clj 3. update server to use http-kit/run-server instead of run-jetty 4. update server and client as per https://github.com/ptaoussanis/sente 5. lein figwheel. the code compiles and the app looks to work mostly fine, but requests over /chsk are all coming back as 403 forbidden. https://github.com/jollyblondgiant/clj-pokedex. what do i need to do to finish sente setup?

Noah Bogart19:11:13

you’re not using the anti-forgery wrapper

Noah Bogart19:11:30

i’m not sure how reitit-ring works, but this part of the sente readme seems to be missing:

(def my-app
  (-> my-app-routes
      ;; Add necessary Ring middleware:
      ring.middleware.keyword-params/wrap-keyword-params
      ring.middleware.params/wrap-params
      ring.middleware.anti-forgery/wrap-anti-forgery
      ring.middleware.session/wrap-session))

Noah Bogart19:11:23

i see some of the middleware in your pokedex.middleware namespace, but you’re missing some or all of these

Andy Carlile19:11:29

I added all of those and the behavior persisted

Andy Carlile19:11:37

(def app
  (let [handler (wrap-defaults #'app-routes site-defaults)]
    (if (env :dev)
      (-> handler
          ring.middleware.keyword-params/wrap-keyword-params
          ring.middleware.params/wrap-params
          ring.middleware.anti-forgery/wrap-anti-forgery
          ring.middleware.session/wrap-session
          wrap-exceptions

          wrap-reload)
      handler)))

Andy Carlile19:11:07

there seem to be no guides to using sente in an app created from the reagent template. is this just not done or is it possible?

Noah Bogart19:11:56

it’s absolutely possible, i run an app that does it

Noah Bogart19:11:10

i don’t know anything about reitit tho so that’s what i don’t know how to help

Andy Carlile19:11:59

how do I get started? I've been trying for days. how do I start a reagent app without reitit, or replace it?

Andy Carlile19:11:10

just checked, I've removed references to reitit from the handler. still receiving 403's in the browser for /chsk

Andy Carlile19:11:36

what was your process to get stood up?

Noah Bogart19:11:19

: #'ring.middleware.anti-forgery/*anti-forgery-token*

Noah Bogart19:11:27

something is wrong with your anti-forgery-token

Andy Carlile19:11:20

is this a string I need to invent or is it supplied from the library?

Andy Carlile19:11:56

i've updated the repo to reflect my most recent flailing about

Noah Bogart21:11:24

okay, i’ve spent too long poking at this but for some reason, :websocket? isn’t being set on the ring requests, so sente returns nil

Noah Bogart21:11:32

that’s the best i’ve got for you, lol

Andy Carlile21:11:47

is that something that gets set in the client or the server?

Noah Bogart21:11:51

i have no idea when/where it would be removed

Andy Carlile21:11:46

good news: it's not my fault? bad news: what do I do now

Noah Bogart21:11:20

lmao i have no idea

Noah Bogart21:11:40

that template is complicated and i don’t understand most of what’s happening

Noah Bogart21:11:49

maybe try starting over and not using a template

Andy Carlile14:11:33

I am able to build a webserver from scratch or a frontend app from scratch but am at a loss at how to get them to compile together. everything I can find (please do believe I have been looking for days-- perhaps I just don't know where to look) starts with a template somehow, or otherwise only refers to the front- or back- end, but rarely how to roll your own full stack

Noah Bogart19:11:56

given Stuart Sierra’s warning against concat (here: https://stuartsierra.com/2015/04/26/clojure-donts-concat) and that the proposed solution wasn’t ever added to Clojure core, what are the best ways to avoid blowing the stack while still getting the functionality of concat?

Noah Bogart19:11:48

do you have an example of how you’d do that?

hiredman19:11:33

(for [i x ii i] ii)

hiredman19:11:56

I mean, it really depends

Mno19:11:09

dunno... lazy-cat?

hiredman19:11:34

nah, lazy-cat is just as susceptible

Ben Sless21:11:13

(defn -concat [& args] (->Eduction cat args)) Just be sure to not do side effects lazily there

Ben Sless08:11:38

Can also (sequence cat args), I think

dpsutton19:11:56

don’t be lazy. Every time i’ve hit this i’ve been able to just use a (into [] ...) or whatever. Not saying that’s always the fix but that has worked for me

👍 1
Muhammad Hamza Chippa19:11:59

I have an array (5 6 8 9) and I want to calculate the difference of consecutive values like (5 (6-5) (8-6) (9-8)) = (5 1 2 1) How can I calculate that ?

Noah Bogart19:11:49

user=> (def l '(5 6 8 9))
#'user/l
user=> (->> l (cons 0) (partition 2 1) (map (fn [[n1 n2]] (- n2 n1))))
(5 1 2 1)

🙌 1
Noah Bogart19:11:18

prepend a 0, partition by 2 stepping 1 at a time, map over the pairs and subtract the second from the first

🙌 1
dpsutton19:11:21

(let [data [5 6 8 9]] (map - data (cons 0 data)))

🙌 1
clojure-spin 1
😯 1
Noah Bogart19:11:51

oh map over multiple lists is clever

Muhammad Hamza Chippa19:11:33

Thanks brother for helping me out

👍 1
mg19:11:39

reductions could be a handy way to go too

🙌 1
mg21:11:38

neevermind

🙌 1
janezj21:11:43

I have found a very strange case and I hope it's a bug not a feature. Last sexp fails.

(def t (Integer/parseInt "2"))
(= (type t) (type (Integer/parseInt "2")))

(-> (java.time.Instant/now)
    (.minus (Integer/parseInt "2") java.time.temporal.ChronoUnit/DAYS))

(-> (java.time.Instant/now)   
    (.minus t java.time.temporal.ChronoUnit/DAYS))
; Execution error (IllegalArgumentException) at numbers/eval24676 (REPL:24).
; No matching method minus found taking 2 args for class java.time.Instant

ghadi21:11:32

use Long/parseLong

ghadi21:11:59

and use (set! **warn-on-reflection** true) to see more about what's going on

janezj21:11:13

but way it not the same result in both cases? why first case is ok, and not the second?

hiredman21:11:21

warn-on-reflection will show you

hiredman21:11:08

the second case is reflective, the compiler doesn't have the type information

hiredman21:11:38

let bindings and locals being immutable preserve type information when they can, but vars (defs) are mutable and assumed to be Object unless you say otherwise

janezj23:11:53

@U0NCTKEV8 I think I understand the problem of clojure maping vars to method arguments, so I try to type hint var and I failed. This is not the solution. (def t ^Integer (Integer/parseInt "2")) (defn ^Integer ft [] t)

hiredman23:11:56

yes, the other issue is boxing

hiredman23:11:47

when selecting a method, if the argument is primitive the compiler will convert integers to longs, but not when they are boxed Integer and Long

janezj23:11:45

@U050ECB92 I developed a nice app, it all worked well, but then I added cli. https://github.com/clojure/tools.cli In the example is the evil Integer. Next I spend too many hours to solve this puzzle (without truly understand what is going on), and declared myself and clojure to be non-compatible. This kind of problems are killing my productivity.

hiredman23:11:52

it is easy to solve (long t)

hiredman23:11:35

or even as @U050ECB92 suggested, use Long/parseLong (to get a Long instead of an Integer, the minus method is documented to take a long anyway)

janezj23:11:11

@U0NCTKEV8 I know the solution, but I didn't understand how compiler works. I was totally shocked that extracting the expression and binding it to var changed the outcome. This is so illogical to me, even now. var is to me a substitution. It seems that var and function results are always boxed, because this type hints are not changing the outcome. (def t ^int (Integer/parseInt "2")) (defn ^int ft [] t

hiredman23:11:21

correct, and ^int is not a valid type hint

janezj23:11:24

a = b + c + d f = c+d a = b + f this is not universal in clojure

hiredman23:11:29

vars are always references, so you can only type hint them as reference types

hiredman23:11:13

if you used the correct type, a long, parsed using Long/parseLong, boxed as a Long, this would all just work

janezj23:11:14

This is the reason I hoped that i found a bug, but it seams it is a reality.

hiredman23:11:09

like, there is no minus method that takes an int or an Integer

hiredman23:11:46

so no matter what you do with integers, the method is going to be compiled reflectively, and when it gets to being called, the reflector is going to be like "there is no matching method"

hiredman23:11:42

and you cannot cast a boxed Integer to a boxed Long, there is no subtype relationship which is required for casting reference types on the jvm

janezj23:11:45

it will be logical to me if this would failed too; (.minus (Integer/parseInt "2") but it is working.

hiredman23:11:47

user=> (cast Integer (long 1))
Execution error (ClassCastException) at java.lang.Class/cast (Class.java:3889).
Cannot cast java.lang.Long to java.lang.Integer
user=> 

hiredman23:11:07

because in that case, the result of the method call is a primitive int

janezj23:11:08

I learned a lot

hiredman23:11:17

and primitive ints can be cast to primitive longs

janezj23:11:17

yes understand it. I feel much better now.

hiredman23:11:26

(my cast example is slightly confusing there, because generally you think of long as returning a primitive long, which it does, but cast is a function so its arguments need to be references so the primitive long is boxed)

hiredman23:11:49

(set! **warn-on-reflection** true) is a really useful thing to use, because if you don't get any warnings from it, that means the compiler matched on the java method calls in the code it is compiling

hiredman23:11:56

user=> (def ^Integer t 1)
#'user/t
user=> (-> (java.time.Instant/now) (.minus t java.time.temporal.ChronoUnit/DAYS))
Reflection warning, NO_SOURCE_PATH:1:1 - call to method minus on java.time.Instant can't be resolved (argument types: java.lang.Integer, java.time.temporal.ChronoUnit).
#object[java.time.Instant 0x4d4960c8 "2021-11-16T23:46:10.018774496Z"]
user=> (type t)
java.lang.Long
user=>
here's an example where it worked, even though it was typed hinted Integer

hiredman23:11:35

that is because at compile time the compiler couldn't match the method with the Integer type hint, so it compiled the call reflectively

hiredman23:11:02

and at runtime the reflective call saw that the argument was actually a Long and matched the method