Fork me on GitHub
#beginners
<
2020-07-31
>
ozzloy00:07:14

is there something like lein clean for cli tools? clj -A:clean or something?

seancorfield00:07:53

That will recompute the classpath from scratch and overwrite the local cache file with the recomputed information.

seancorfield00:07:20

You have to use it with the same aliases as you want to run anyway, not just on its own.

seancorfield00:07:37

clojure -Sforce -A:usual:stuff:here

seancorfield00:07:05

The cached information is per-unique-combination-of-aliases.

ozzloy00:07:05

oh, i just used lein clean to make the output of tree shorter

seancorfield00:07:35

Oh, well, the CLI tools don't produce a target folder full of stuff.

seancorfield00:07:55

I assume that's the tree you are talking about?

ozzloy00:07:03

maybe it's figwheel that is creating target

seancorfield00:07:18

Probably. Compiling .cljs files to .js ?

ozzloy00:07:01

i assume. there's a target/public/cljs-out. seems like a safe bet.

ozzloy00:07:02

oh hey, i tried creating src/foo.cljs and src/foo.cljc and fired up a repl and was not able to see the things in foo.cljc

seancorfield00:07:37

I don't do any ClojureScript so I can't help with that.

walterl00:07:11

What's the idiomatic way perform a bunch of steps on a map, and recording the results of each step in the map, so that the results are available to the next step? I imagine some like this:

(assoc-> {} $
  :foo 1
  :bar (inc (:foo $))
  :baz (inc (inc (:bar $))))
It's inspired by as->, and can be implemented in terms thereof:
(as-> {} $
  (assoc $ :foo 1)
  (assoc $ :bar (inc (:foo $)))
  (assoc $ :baz (inc (inc (:bar $)))))
Or with reduce:
(reduce (fn [m [k f]] (assoc m k (f m)))
        {}
        [[:foo (constantly 1)]
         [:bar #(inc (:foo %))]
         [:baz #(inc (inc (:bar %)))]])
Either solution can be used to implement an assoc-> macro. But then, looking at the as-> solution, it made me wonder if it's worth it: is the cost of introducing a new macro (`assoc->`) into the vocabulary, worth the small saving in cruft of the as-> solution? My gut says "no", and yet this seems like the kind of common use case for which there should be something more idiomatic.

seancorfield00:07:46

I'd probably prefer something with reduce if I wanted the previous map to be available for the next step. And I might be tempted to use point-free style if I thought it helped readability, and maybe partition so I could avoid the nested pairs...

seancorfield00:07:39

(reduce (fn [m [k f]] (assoc m k (f m)))
        {}
        (partition 2 [:foo (constantly 1) :bar (comp inc :foo) :baz (comp inc inc :bar)]))

seancorfield01:07:40

Not sure I'd bother with a macro for that @clojurians-slack100 since a function would do just fine:

(defn assoc-f [base & args]
  (assert (even? (count args)))
  (reduce (fn [m [k f]] (assoc m k (f m)))
          base
          (partition 2 args)))

(assoc-f {} :foo (constantly 1) :bar (comp inc :foo) :baz (comp inc inc :bar))
Something like that?

ūüėć 1
seancorfield01:07:08

user=> (assoc-f {} :foo (constantly 1) :bar (comp inc :foo) :baz (comp inc inc :bar))
{:foo 1, :bar 2, :baz 4}
user=> 

walterl01:07:35

Wow! Very cool! ūüĎŹ

walterl01:07:03

Function or macro, though, the question remains: does that complicate reading of the code more than using a slightly messier as-> would?

seancorfield01:07:50

I think the as-> example you gave is a lot less readable than the one line (assoc-f ...) call above ūüôā

seancorfield01:07:55

(in general, I only use as-> inside a -> pipeline for small expressions that don't follow the first-arg threading)

ūüĎć 1
seancorfield01:07:50

(-> (something)
    (f arg2)
    (as-> m (assoc m :foo (inc (:bar m))))
    (g arg2 arg3))

seancorfield01:07:18

(and if I'm using as-> at all, I prefer a less cryptic name than $ or % or whatever)

seancorfield01:07:21

As for macros, consider them a "last resort": always try to do things with a function first. Add a macro later just for syntactic sugar.

ūüĎć 1
seancorfield01:07:13

Unless you have a situation that absolutely requires arguments are not evaluated -- then you have to write a macro. But even then, try to implement it under the hood as a function that is perhaps passed a "thunk" (a 0-arity function).

walterl01:07:43

Great answer(s), thanks @seancorfield

vlad_poh04:07:35

how do i speed up "lein ring server" api calls are about 10-20x slower

vlad_poh04:07:56

and restarts take like 3 super long minutes

seancorfield04:07:51

10-20x slower than... what @kbosompem?

vlad_poh04:07:33

building an uberjar and running it

seancorfield04:07:02

Do you have development middleware doing reloading etc?

seancorfield04:07:44

Hard to give you much guidance without more details about exactly what you have in play and what you're comparing it to... ūüôā

seancorfield04:07:17

Normally, running code from source will be slower to start up (than an uberjar) because it all has to be compiled as it is loaded (whereas the uberjar has everything pre-compiled) but then it should be the exact same speed in both cases once it is running.

seancorfield04:07:52

Even with that compilation overhead at startup, I would be surprised by a three minute startup time unless you had a pretty big, complex app...

vlad_poh04:07:22

its a simple ring app

vlad_poh04:07:39

lein ring server let's me make changes and without reloading

vlad_poh04:07:13

but every so often when i make changes in hugsql i am forced to restart it

seancorfield04:07:28

Sounds like it has middlware that automatically watches for code changes and reloads it for you?

seancorfield04:07:49

I never used the ring plugin with lein. I just have code to start the Jetty server directly in -main, and I can also easily start it from the REPL. So I've no idea what the plugin might be doing that's taking so much time.

ūüĎć 1
alexmiller04:07:02

getting all of your deps aot compiled and on the classpath would probably make a huge difference in startup time

vlad_poh04:07:04

ooo looks interesting. I've never seen that guide will go read it asap

seancorfield04:07:31

I pretty much never need to reload code. I evaluate code directly into the running app so I can avoid that. And although I don't use HugSQL, I would have expected not to need a reload just because I changed the SQL files -- you should be able to ask HugSQL to redefine the functions and have those new definitions in play immediately in the running REPL...

seancorfield04:07:04

I generally advise beginners to avoid plugins and any special middleware that tries to auto-reload code: there's a lot of "magic" going on in those sorts of tools that can mask (or even cause) all sorts of problems and make things hard to debug.

alexmiller04:07:50

you might also check your Java version - it's also possible you're hitting the Java regression we saw around Java 1.8u202 / 11.0.1 which primarily manifests as very slow startup when using user.clj. Newer versions of those did have some improvements but really using Clojure 1.10.1 has changes to sidestep the regression.

seancorfield04:07:35

Ah, I didn't even think about user.clj -- something else I avoid ūüôā

alexmiller04:07:45

often in play with this kind of ring setup

vlad_poh04:07:04

repl starts in 30 secs. going to try that

alexmiller04:07:23

I mean, the user.clj is probably the thing loading all the code in this case

seancorfield04:07:44

I'm curious as to how much the Cognitect folks lean on user.clj when they're developing? I think I've only ever used it once, in just one project, and that was to compile a specific ns when a REPL started up so that a (:gen-class) type would be available to the rest of my code.

alexmiller04:07:14

probably varies, but Rich, Stu, and I never use it

seancorfield04:07:32

That's what I figured. We need y'all to do more talks about your dev workflow and your use of the REPL. It seems to me that a lot of beginners go down the rabbit hole of all sorts of weird plugins and add-ons and complex, error-prone "reloaded" / auto-refresh tools ūüėě

‚úĒÔłŹ 3
alexmiller04:07:52

well Stu did a pretty good one :)

alexmiller04:07:04

there's not much to talk about if you keep it simple :)

alexmiller04:07:33

"here's me not using lein or any plugins and just starting a repl"

alexmiller04:07:00

but also, our use cases are different than most Clojure devs doing web stuff.

seancorfield05:07:24

I dunno. I do web stuff (backend) all day, every day and my workflow is like y'all's ūüôā

seancorfield05:07:46

Stu's talks are what I refer people to, and the podcast he was on, talking about his workflow.

seancorfield05:07:04

And Eric's "RDD" course is awesome for that too.

seancorfield05:07:39

But, yeah, no plugins, no refresh, no watchers, no auto-reloading, no user.clj.

seancorfield05:07:06

And I gather not much use of CIDER/nREPL at Cognitect either, I'll warrant?

vlad_poh04:07:26

i have a user.clj but i fear you maybe referring to something else.

vlad_poh04:07:34

does it just look for any "user.clj"

seancorfield04:07:52

Does it have a (ns ...) form in it, or is it just a top-level script with some development-related code in it?

vlad_poh04:07:36

its in src > clj > vg > api and has the (ns ...)

seancorfield04:07:12

OK, then not the user.clj file that Alex was referring to.

vlad_poh04:07:23

wow thank you thank you @seancorfield and @alexmiller did the "compile" and run jetty from the repl and it was instantaneous

vlad_poh04:07:09

Same call went from 6s on average in dev mode to 1.2s which is close enough to the 800ms with a jar

seancorfield04:07:55

My preference is to keep my dev setup as close to my production setup as possible: no plugins, no dev-only middleware, no watchers, no auto-reloading -- just a tight REPL workflow.

ūüĎć 1
seancorfield04:07:33

(although I do build an uberjar with AOT compiled code for production deployment -- but I still run the same Socket REPL in both production and local dev)

vlad_poh04:07:17

while i have both of you here one more question. I use timbre for logging and i like it but adds at least half a second to each function call so i wrapped the call in a future to "speed" things up. unfortunately now the log isn't sequential. what would you recommend?

alexmiller04:07:52

a half second sounds unusable to me

alexmiller04:07:31

some Clojure loggers use an agent - should be basically sequential (per concurrency constraints) due to the agent queue. not sure what timbre has

seancorfield04:07:43

No idea why Timbre would take so long. What do you mean by "half a second to each function call"? You mean just adding logging calls?

vlad_poh04:07:57

i assumed the slowness was due to the database appender i enabled

vlad_poh04:07:04

not part of the core library

seancorfield04:07:54

Oh... well, possibly. We used Timbre at work extensively but never used its database appenders. Perhaps it is setting up a new connection on every call, instead of using a connection pool?

seancorfield04:07:51

If you can use a connection pool with that database appender, I'd expect it to be much faster.

seancorfield04:07:28

What's the DB appender library you're using?

vlad_poh05:07:54

mysql-appender

vlad_poh05:07:19

(defn mysql-appender
  ""
  [db-config]
  {:enabled?   true
   :async?     false
   :min-level  :info
   :rate-limit nil
   :output-fn  :inherit
   :fn (fn [data] (log-message db-config data))}) 

vlad_poh05:07:39

(defn log-message
  "Log application messages in application database."
  [config data]
  (let [entry
        {:instant   (java.sql.Timestamp. (.getTime ^java.util.Date (:instant data)))
         :level     (str        (:level     data))
         :line      (str        (:?line     data) " | " (:tid data))
         :vargs     (str        (:vargs     data))
         :namespace (str        (:?ns-str   data))
         :content   (str (force (:msg_      data)))
         :error     (str        (:?err      data))
         :hostname  (str (force (:hostname_ data)))}]
    (jdbc/insert! config :logs entry)))

seancorfield05:07:43

Triple backtick at the start and end of code makes it a lot more readable.

seancorfield05:07:36

:thumbsup::skin-tone-2:

vlad_poh05:07:21

(def log-config
  {:level :info
   :appenders {:mysql-appender (mysql-appender db)
               :rolling-appender (rolling-appender {:path log-file-name :pattern :daily})}})

seancorfield05:07:25

So, yeah, your mysql-appender should make a connection pooled datasource from db-config and use {:connection cpds} instead of db-config in the log-message call.

seancorfield05:07:32

(for what it's worth, next.jdbc is the replacement for clojure.java.jdbc and has built-in support for connection pooled datasources with both c3p0 and HikariCP)

seancorfield05:07:05

And HugSQL supports next.jdbc -- there's a getting started with HugSQL guide in the next.jdbc docs.

seancorfield05:07:08

Overall, next.jdbc is faster than clojure.java.jdbc in general usage...

vlad_poh05:07:58

will do! this has been super helpful.

seancorfield05:07:40

If you have any questions about next.jdbc (or about clojure.java.jdbc -- I maintain both), drop into the #sql channel and ask (I'm more likely to see questions there).

ūüĎć 2
vlad_poh05:07:12

will definitely do!

Jim Newton10:07:23

What is the best way to invert a filter? Such as the following:

clojure-rte.core> (filter :accepting [{:accepting true
                                      :a 100}
                                      {:accepting false
                                      :a 200}
                                      {:accepting true
                                      :a 300}])
({:accepting true, :a 100} {:accepting true, :a 300})
clojure-rte.core> 

Jim Newton10:07:19

what I really want to do is partition the sequence by a predicate, binding the true responses to one variable and the false responses to the other.

(let [[trues falses] (partition-by-predicate some-sequence some-predicate)]
  ...)
I'm tempted to call filter twice, once with the predicate and second with the complement of the predicate

leonoel10:07:14

the complement of filter is remove, but in this case you probably want group-by :

(map (group-by :accepting coll) [true false])

Jim Newton10:07:38

(map (group-by ...) [true false]) that's clever

Jim Newton10:07:56

another question. I'm implementing a function which mathematically returns a set of sets. So I don't care which order the elements are generated. i.e., [[1 2 3] [4 5]] is semantically equivalent (for my program) to [[5 4][1 3 2]]. I've implemented this forcing the results to be sets, which makes the test cases easy to write. However, when I map and filter using sets, I don't get back sets. If I want to write my functions using just default clojure sequences, what's the correct way to write my tests?

Jim Newton10:07:26

(defn %partition-by
  "Given a set of objects, return a set of subsets thereof which is a partition of
  the given set.   Every element in any some set has the same value under f, and
  the value under f is different for any distinct subsets.  f is not called
  if the size of the given set is 1."
  [objects f]
  (if (= 1 (count objects))
    #{ objects}
    (set (map set (vals (group-by f objects))))))


(deftest t-%partition-by
  (testing "%partition-by"
    (is (= (%partition-by #{1 2 3 4 5 6 7} even?)
           #{#{1 3 5 7}
             #{2 4 6}}))
    (is (= (%partition-by #{1 2 3 4 5 6 7 8 9} #(mod % 3))
           #{#{2 5 8}
             #{1 4 7}
             #{3 6 9}}))))

Jim Newton10:07:52

BTW, I also discovered that (filter (complement :accepting) ...) also works

noisesmith18:07:28

it works, but (remove :accepting ...) does the same thing, and that's why remove exists

Jim Newton11:07:23

I'd like to iterate over a sequence, (for example using (map f sequence) and have the iteration function f return a key/value pair, to generate a map, rather than a sequence.

manutter5112:07:45

You can just return a 2-element vector, (into {} [[:a 1] [:b 2] [:c 3]]) -> {:a 1, :b 2, :c 3}

manutter5112:07:00

Or to better fit your example:

(into {}
      (map (fn [x] 
             [x (inc x)]) 
           [1 2 3]))
=> {1 2, 2 3, 3 4}

ūüĎć 1
Jim Newton13:07:55

do we have an array tabulate function? I.e., I want to create an array of size N where rather than giving explicitly N values for the array, instead I give a function which maps an integer the intended array value? Of course I can write such a function, but want to re-use what's there. I guess it would be something like this?

(defn tabulate
  ""
  [n f]
  (into [] (map f (range n))))

alexmiller13:07:14

map-indexed might be helpful?

Jim Newton15:07:07

@alexmiller I didn't know about map-indexed thats sort of a cousin of zip-with-index I guess. BTW, my guess is that (into [] (map f (range n))) is a pretty good solution. right?

dpsutton15:07:58

i often do (map vector (range) coll) to end up with tuples of index and items from the collection

dpsutton15:07:04

because i forget the order of args for f in map-indexed. so i just make my own tuples

Jim Newton15:07:35

I thought an expression like (some #{target} sequence) works as a Boolean to determine whether target is an element of the sequence. It doesn't always work. what should I be using instead?

Jim Newton15:07:56

if target is nil the (some #{nil} [ 'a 'b nil 'c]) returns Boolean false

alexmiller15:07:28

generally, I would either use a set or a map instead and avoid the linear search

Jim Newton15:07:12

@alexmiller, thats'a response to which comment?

dpsutton15:07:45

Returns the first logical true value of (pred x) for any x in coll,. #{nil} as a predicate can never return logical true.

Jim Newton15:07:18

Also (some #{false} ['a 'b false 'c]) returns nil , which surprises me.

alexmiller15:07:46

just as a general thing, don't use a set as a predicate if you are trying to match logically false values (`nil` or false)

alexmiller15:07:02

this is just one example of that more general statement

Jim Newton15:07:06

@dpsutton. yes I see. good explanation, thanks. So what is the correct way to search for an item in a sequence?

alexmiller15:07:43

don't search an item for a sequence - make a keyed data structure

alexmiller15:07:04

or use some (but be beware of the caveats above)

alexmiller15:07:26

or combo of first / filter is done sometimes

Jim Newton15:07:41

I'm using an array, because I need O(1) access.

dpsutton15:07:00

you access n items in O(1) time then. so its still linear

dpsutton15:07:24

(for the linear contains search)

Jim Newton15:07:44

it is indeed linear when I'm scanning it during construction. However, after I've constructed, it is O(1)

dpsutton15:07:05

but (some #(contains? #{nil false} %) [1 2 nil 3]) can do the search for logical false values

Jim Newton15:07:14

my current challenge is the construction phase. I don't mind that it is O(n)

Jim Newton15:07:46

So given a variable x which has been passed to me from the user of my library, How can I know if that value is in my array?

alexmiller15:07:11

I think we're backing into the real problem "what data structure should I use given access pattern X"

Jim Newton15:07:33

I can of course iterate over the array and test every value == x

alexmiller15:07:49

given only that description I would say, use a map and look it up

Jim Newton15:07:53

I'm a bit shocked that the sequence api does not have membership as an operation.

alexmiller15:07:07

it's quite intentional

alexmiller15:07:25

because you should use a data structure that supports that question better

Jim Newton15:07:31

My philosophy is that you should choose the data structure based on what makes the most sense for the entire program. not just to make it easy to construct.

Jim Newton15:07:13

But anyway, I understand the limitation. I don't have to like it, I just have to understand it.

alexmiller15:07:28

that seems right, but seems like you're choosing a data structure that is slow to answer the operations you are doing on it (not construction)?

dpsutton15:07:39

for the entire program would include o(1) membership checking and you're missing that feature in your data structure

Jim Newton15:07:14

I don't need membership checking after the object has been constructed.

Jim Newton15:07:44

after it has been constructed all operations are done by array index.

Jim Newton15:07:31

The construction of my object is already exponential worst-case. So I don't mind an additional linear search.

Pablo19:07:03

Hi! Is there a way to add a custom task into the lein clean process?

noisesmith19:07:12

as far as I know, all you can define is extra :clean-targets to clean up, for arbitrary tasks I'd define a new alias that calls clean along with your other tasks

noisesmith19:07:41

:aliases {"my_clean" ["do" "clean" "foo" ["bar" "bar-arg"]]}

Pablo20:07:41

Ok ūüėě Thanks!

ghadi20:07:01

(do (for [i [1 2 3]] (println i))
 nil)
@p.kushwaha97 ^ same effect here, no macro

ghadi20:07:19

for is a list comprehension, generating a lazy sequence

ghadi20:07:00

the sequence is only evaluated if something demands it

peey20:07:41

I see. Can I force demand it (for debugging)? Usually to get rid of compiler optimizations you'd do something with side effects (like printing it out) and that'd be sufficient.

noisesmith20:07:01

it's not an optimization - for isn't a loop, it's a lazy generator

ghadi20:07:14

conceptually you get

(do <LAZY-THING>
  nil)
with the block above

ghadi20:07:36

you can demand things by consuming the seq -- first/rest, or pour it into a vector:

ghadi20:07:44

(vec <LAZY-THING>)

peey20:07:46

I suppose then even (println <LAZY-THING>) would work?

ghadi20:07:52

vectors are always realized

noisesmith20:07:28

there's also dorun for forcing side effects while discarding the value (but at that point why were you using a lazy thing in the first place?)

ghadi20:07:33

everything in Clojure is eagerly evaluated, but you have facilities to make lazy/infinite lists

peey20:07:24

I think this solves my current problem. I'll read up more on how clojure handles lazy evaluation and dorun. Thanks everyone!

seancorfield20:07:46

(doseq [i [1 2 3]] (println i)) ; just for side-effects
(run! println [1 2 3]) ; map-like function for running side-effects

peey20:07:01

Actually, doseq fits better than for. I now understand what @noisesmith was saying about why I'm using lazy things in the first place. I'm coming from common lisp / racket so I assumed that "for" was similar to the loop macros there. Should've read docs more carefully.

aaroncoding20:07:22

Hey again. Looking into routing in a new ring app... I looked at reitit and compojure, but in the end, I kinda prefer just plain old core.match. Passing in the request method and uri (split on "/"). Is there any good reason to NOT do that? Are there security concerns that the routing libraries handle?

noisesmith20:07:04

routing libs can give conveniences like route parameters and reverse routing (eg. "construct a route to handler x" as a markup helper), but I don't know of any big security gotchas with home grown routing

noisesmith20:07:33

btw compojure is far from the best routing lib out there IMHO - people like it for the syntactic sugar, but there are quite a few other options

aaroncoding20:07:05

Ah maybe I just had a bad first experience. What ones do you like?

noisesmith20:07:28

bidi and reitit are good

noisesmith20:07:28

bidi's readme has a feature comparison matrix https://github.com/juxt/bidi

seancorfield20:07:29

I would not expect core.match to perform particularly well compared to dedicated routing libs -- and I'd also note that it hasn't had a functional update in about three years (and is considered "stable" which means there's no active development being done -- https://clojure.org/community/contrib_libs ). Not that "stable" is bad in anyway.

seancorfield20:07:19

We mostly use Compojure at work, with one app using Bidi. If we were starting over, we'd probably use Bidi or Reitit for all of our apps I think.

aaroncoding20:07:08

Reading Bidi readme now. I didn't know about that one

noisesmith20:07:21

I think their feature matrix gives a fair overview for comparison too - data oriented and reverse routing and clj/cljs compat are three huge ones IMHO

seancorfield20:07:24

Reitit would probably be my first choice at this point. The Metosin folks are obsessed with performance and it works well with clojure.spec which we use very heavily.

aaroncoding20:07:51

I'm struggling to understand the use case of reverse routing in a practical sense

seancorfield20:07:00

(I say that from zero experience with it -- but I'd evaluate it as a first choice)

noisesmith20:07:26

@coding.aaronp if you want to link from one page to another, reverse routing means you can link based on feature, and not a specific site map

noisesmith20:07:52

which means that if marketing decides the routes need to be prettier or whatever, the link generation handles the change

seancorfield20:07:55

Yeah, it's very useful if you are refactoring your routing during development.

hiredman20:07:08

if I was going to roll routing from scratch I would think about starting from a parser

aaroncoding20:07:09

Oh, so you might actually use it outside of the router itself? Like (static-path "foo.js") to create a route to wherever

noisesmith20:07:30

right - that's part of why the router working from both clj and cljs is good

aaroncoding20:07:48

Well that's very interesting. Thanks!

noisesmith20:07:59

but that also applies with back-end pre-rendering with links

robertfw20:07:09

@coding.aaronp To add another data point - I am using bidi currently at work, but if I were to start a new project today, I'd definitely go for reitit

robertfw21:07:07

Both are data driven, which is great. When we started the project we were using Compojure, but did not like using macros. reitit takes inspiration from bidi in being data driven, and I think improves in terms of provided functionality, developer ergonomics. metosin do a lot of work in this area and have several excellent tools in their open source ecosystem, and reitit provides modules for integrating with many of them.

Eric Ihli21:07:45

What's the best way to quickly iterate on Clojure macros that are used in ClojureScript? I'm having to restart my shadow-cljs process every time I make a change for it to take effect. Is there a better way? I have this in a clojure file.

(defmacro parse-file [file]
  (keywordize-keys
   (toml/parse-string (slurp (io/resource file)))))
And then I'm doing a (:require-macros) in my ClojureScript file and then (def data (parse-file "example.toml")) . Everything is working fine except that when I edit the .toml file the changes don't appear until I restart my shadow-cljs watcher. (I know this could be fixed by making an xhr request rather than inlining the data with a macro.)

seancorfield21:07:59

@ericihli Maybe a question for #shadow-cljs or #clojurescript ?

ūü§ô 1
jsn22:07:55

hrm, just spent about half an hour banging my head against the wall -- apparently protocol methods can't accept varargs. I think it should have been mentioned somewhere here https://clojure.org/reference/protocols

jsn22:07:52

so, if I want to implement my own reftype (via deftype ), and I want it to work with swap! -- my only option is to implement clojure.lang.IAtom , including each and every arity of IAtom 's swap ?

noisesmith22:07:36

they allow varargs, but your implementation has to cover each arity from the original, or fail at runtime

noisesmith22:07:00

in practice that just means one vararg impl, and a bunch of other impls that call it, most of the time

noisesmith22:07:40

also IAtom is an interface, not a protocol, and it only has 4 arities

noisesmith22:07:10

this isn't about protocols, and for the most part it's not even defined by clojure, the VM dispatches methods by signature, and each arity that is defined in an interface is a separate signature for dispatch

jsn22:07:50

@noisesmith what do you mean "they allow varargs"? (defprotocol RefVal (swap! [this f & args])) doesn't seem to do anything useful

jsn23:07:58

AFAIU it now, it says "has method "swap!" that takes "this" and 3 args ("f", "&" and "args"), so it's not varargs

noisesmith23:07:45

OK - I misremembered that

noisesmith23:07:02

but that's not even relevant to IAtom which is an Interface, not a protocol

jsn23:07:16

Ah, ok. So yeah, I can have many arities, but not varargs

jsn23:07:41

My idea was to bypass IAtom, and have my swap! via my own protocol

noisesmith23:07:47

clojure.core/swap! turns arities into a call with a seq

noisesmith23:07:53

clojure.core/swap! is varaargs

jsn23:07:57

yeah, I know

noisesmith23:07:13

then do what clojure.core did, and make a function calling your protocol

noisesmith23:07:23

but why do you even need a protocol if you aren't using IAtom?

jsn23:07:28

but I can't change existing swap! ?

jsn23:07:20

Ok, let's rewrind. I wanted to have my own ref type that looks like an atom (but is not really an atom)

noisesmith23:07:36

I think having to make your own fake swap! via your own protocol is a much hackier alternative compared to just supporting a few arities of IAtom / swap

jsn23:07:15

well, if not for varargs, the protocol approach seems very straightforward to me

jsn23:07:59

Anyway, so I take it as a "yes" ("I have to re-implement all arities of IAtom's swap "), right?

noisesmith23:07:32

the point of protocols / interfaces is to be able to trade out implementations, if what you are doing is a parallel incompatible implementation, what is the benefit of a protocol?

noisesmith23:07:56

you need to support all the arities that could meaningfully be called, none will be auto-generated

noisesmith23:07:14

but if they never get called, then clojure and the vm dont' care that they aren't implemented

jsn23:07:45

the benefit of protocol, in this particular case, would be that I could have the same user-visible API without being tied to IAtom (which my new ref type isn't)

noisesmith23:07:55

you don't need a protocol for that

jsn23:07:08

don't I?...

noisesmith23:07:19

function plus deftype suffices

jsn23:07:44

But I can't have a function, because swap! is already there in core?

noisesmith23:07:46

but I still think the easier thing is to just implement the 4 or 5 arities

noisesmith23:07:10

swap! won't work anyway, if you don't use the proper interface, please don't go and monkey patch core

noisesmith23:07:19

I mean, I can't stop you, but it's a bad idea

jsn23:07:45

swap! via a protocol does work, just not with varargs

noisesmith23:07:47

you can make a fake swap function as easily (easier) than you can make a fake atom protocol

jsn23:07:11

I can make a fake swap! , but then I'll lose the real swap! in that namespace, I don't like that

noisesmith23:07:50

user=> (swap! (reify clojure.lang.IAtom (swap [this f] "OK")) inc)
"OK"

jsn23:07:28

yeah, all variants involving IAtom are pretty obvious (but generally involve the "all arities" thing)

jsn23:07:43

anyway, I guess my options are clear now, thanks!

noisesmith23:07:22

the more arg in the last arity fakes varargs

user=> (swap! (reify clojure.lang.IAtom (swap [this f a b more] more)) :a :b :c :d)
(:d)
I guess clojure has to hack around how weird varargs are on methods in interop, and that's why you can't use & in protocol signatures

jsn23:07:20

You wish! The ugly thing is that you can use &, and it treats it like a parameter name ūüôā

walterl23:07:36

@seancorfield: Re this function you wrote yesterday:

clojure
(defn assoc-f [base & args]
  (assert (even? (count args)))
  (reduce (fn [m [k f]] (assoc m k (f m)))
          base
          (partition 2 args)))
Why use an assert instead of a precondition, like this?
clojure
(defn assoc-f [base & args]
  {:pre [(even? (count args))]}
  (reduce (fn [m [k f]] (assoc m k (f m)))
          base
          (partition 2 args)))

seancorfield23:07:59

To be honest, mostly personal preference -- I don't like the :pre/`:post` syntax much @clojurians-slack100

walterl23:07:38

ūüíĮ thanks. Was just curious ūüôā

seancorfield23:07:03

I do use :pre and :post occasionally. In our 100k line code base at work, there are 11 :pre conditions and just one :post condition. There are a lot more assert calls.

seancorfield23:07:32

(333 assert calls in 68 files)

walterl23:07:31

Related curiosity: is there a way in Clojure to turn asserts into nops, like Python's -O3 cmd-line arg?

seancorfield00:08:45

Yes, if you compile your code with *assert* set! to falsey, then assert becomes a no-op.

ūüéČ 1
seancorfield00:08:19

But I think that's a bad idea.

seancorfield00:08:24

If the assert would have triggered in production but you turned it off, instead of getting a hard exception (well, an Error, not an Exception), that bad data continues to flow through your program potentially corrupting your data and/or producing unintended behavior.

ūüĎć 1
seancorfield00:08:05

Leiningen provides a way to do it (`:global-vars` in project.clj). You can turn it off for a session via the Clojure CLI:

(! 1043)-> clj
Clojure 1.10.1
user=> *assert*
true
user=> ^D
(! 1044)-> clj -e '(set! *assert* false)' -r
false
user=> *assert*
false
user=> 

seancorfield00:08:12

If you're using depstar, you can use that -e trick to disable assertions when compiling code for an uberjar (but I don't recommend it ūüôā )

ūüĎć 1
seancorfield00:08:14

(I actually had to test that to be certain it was possible):

(! 557)-> clojure -m hf.depstar.uberjar test.jar -C -m hf.explore
Compiling hf.explore ...
Building uber jar: test.jar
Processing pom.xml for {seancorfield/depstar {:mvn/version "1.0.96"}}
(! 558)-> java -jar test.jar 
Exception in thread "main" java.lang.AssertionError: Assert failed: (pos? x)
	at hf.explore$foo.invoke(explore.clj:5)
	at hf.explore$_main.doInvoke(explore.clj:10)
	at clojure.lang.RestFn.invoke(RestFn.java:397)
	at clojure.lang.AFn.applyToHelper(AFn.java:152)
	at clojure.lang.RestFn.applyTo(RestFn.java:132)
	at hf.explore.main(Unknown Source)
(! 559)-> clojure -e '(set! *assert* false)' -m hf.depstar.uberjar test.jar -C -m hf.explore
Compiling hf.explore ...
Building uber jar: test.jar
Processing pom.xml for {seancorfield/depstar {:mvn/version "1.0.96"}}
(! 560)-> java -jar test.jar 
# runs with no assertion error
(! 561)->

ūüéČ 1
walterl00:08:27

I was just looking at depstar last weekend as a substitute for lein uberjar, in a small side project I'm trying to convert to tools.cli ūüė¨

seancorfield00:08:30

Feel free to DM me with questions about either depstar or tools.cli since I maintain both. For Clojure CLI / deps.edn questions in general, the #tools-deps channel is a great place to ask.

walterl00:08:28

Will do, thanks. I hope to get to that conversion again this weekend ūüôā

seancorfield00:08:43

I'll be online a lot of the weekend (I've been off yesterday and today as well -- nice four day weekend!). We're not early risers but I'm often a night owl -- Pacific time.

1
seancorfield00:08:33

(and I always have my laptop open while watching TV -- we're binge-watching Star Trek: Deep Space 9 and we're into the final season at this point)

walterl00:08:21

I'm on season 6 of TNG ūüôāūüöÄ ūüĆĆ

seancorfield00:08:15

We already did all of The Original Series, then TNG, then into DS9 ūüôā All since quarantine started here in mid-March!

seancorfield01:08:11

(and we did all seven seasons of Star Wars: The Clone Wars animated series before we switched to Star Trek)

stormtrooper 1
jsn23:07:10

That's why I think it should be documented -- it's quite non-obvious

jsn23:07:36

cells=> (defprotocol R (swap! [this f & args]))
cells=> (deftype R' [] R (swap! [this f & args] (println &)))
cells.R'
cells=> (swap! (->R') + 1 2)
1
nil
cells=>
^ this is like seriously non-obvious, I think ūüôā

jsn23:07:24

A nice trick to remember if you're ever in an "underhanded clojure" contest