Fork me on GitHub
#beginners
<
2023-03-02
>
Markus11:03:44

Hello again! I implemented Comparable in a record. I now have a list of records and I want to retrieve the max value. How can I leverage the Comparable interface to do this? Is sort the only option, even though I don’t need a sorted list?

delaguardo11:03:15

There are many ways to do that. For example you can use reduce

(defrecord Smthng [x]
  Comparable
  (compareTo [_ another-smthng]
    (.compareTo x (:x another-smthng))))

(reduce
 (fn [left right]
   (if (pos? (.compareTo right left))
     right
     left))
 (map #(->Smthng %) (repeatedly 20 #(rand-int 100))))

Markus11:03:17

Ah yes, thats perfect! I first thought that the max or max-key function would provide similar functionality, but all I got was an error because map produces a LazySeq

delaguardo11:03:08

both max and max-key work with Numbers only

tomd17:03:31

There's greatest-by and least-by in medley if you don't mind a lib.

Matthew Twomey16:03:48

As I’ve become more and more accustomed to using the repl, I’ve found myself “restarting the whole thing” less and less, which is good and faster and convenient. I wanted to ask what people generally do when they want to add a new dependency to their classpath though? I find when I’m starting something new, I am constantly doing this (oh, I need this… oh, I need that). Each time I restart the repl after. Is this par for the course?

dorab16:03:13

There is an "experimental" git branch (called add-lib3) of tools deps alpha that has some functionality to load a new dependency into a running Clojure REPL. For example usage, see Sean Corfield's dot-clojure Github repo and look for the :add-libs alias.

4
Matthew Twomey16:03:44

Ok thanks, will look. It’s not a huge deal - I was just wondering if I was missing something “simple”.

seancorfield17:03:08

I have an alias for that in my dot-clojure repo if you want to take a look but Alex says there's something coming in Clojure 1.12 that will make this more "built-in"...

🚀 2
👍 2
Alex Miller (Clojure team)19:03:08

Very soon, this is coming to a Clojure 1.12 alpha :)

💯 4
❤️ 8
practicalli-johnny20:03:55

It is possible to only include Clojure and the add-libs dependencies and then hot load all other libraries as needed. When add-libs is built-in then only the Clojure dependencies seems like it's needed. Examples of how I use add-libs to hot load libraries and other useful tools are written up at https://practical.li/clojure/clojure-cli/repl-reloaded/

2
seancorfield20:03:04

Here's an example from next.jdbc's tests of code that loads its :test dependendencies -- so you can hotload those into a REPL while working on a project that depends on next.jdbc but wouldn't have those dependencies (transitive test deps). https://github.com/seancorfield/next-jdbc/blob/develop/test/next/jdbc/test_fixtures.clj#L239-L254

Mario Trost08:03:07

There's also classpath from Lambdaisland: https://github.com/lambdaisland/classpath#watch-depsedn Haven't used it myself, but automates reloading new libs by watching for changes in your deps.edn

simongray09:03:17

Personally, restarting to add or change dependency is not a big deal to me...

teodorlu14:03:15

> wanted to ask what people generally do when they want to add a new dependency to their classpath though? For now, I just restart the REPL. But I'm looking forward to what 1.12 brings!

dgb2317:03:54

I feel like I'm doing something terribly wrong here. My general problem is that I don't really know what I'm doing. Macros are still very foreign to me. In some cases they seem nice and trivial, in others they seem like black magic.

(defmacro table->case [table]
    (let [flattened (flatten (seq (eval table)))] ; eval sounds scary
      `(fn [k#]
         (case k#
           ~@flattened))))

  (def my-data-table
    {:on-a :a
     :on-b :b})

  (macroexpand-1 '(table->case my-data-table)) ; ok
  ((table->case my-data-table) :on-a) ; ok

  (def my-jump-table
    {:on-a (constantly :a)
     :on-b (constantly :b)})

  (macroexpand-1 '(table->case my-jump-table)) ; ok
  ((table->case my-jump-table) :on-a)
  ; No matching ctor found for class clojure.core$constantly$fn__5740

seancorfield17:03:40

I don't think you even need a macro for this -- you can do it with a regular function, yes?

dgb2317:03:17

But how do I generate a function that turns a map (like the ones above) into a case ... ?

seancorfield17:03:16

You have a hash map -- why do you need a case there?

dgb2317:03:14

I don't need it. I wanted to try to generate a function from a map that does static dispatch.

dgb2317:03:29

like a more compiled version of it

dgb2317:03:54

whether that's a good idea, I don't know. But the error message is very confusing regardless!

seancorfield17:03:10

(case k :kw1 fn1 :kw2 fn2) is still a dynamic "lookup"...

dgb2317:03:28

well that settles it then 😄

seancorfield17:03:53

My general advice is to avoid macros unless you specifically need a construct that doesn't evaluate arguments. If you start writing a macro that needs to call eval, you almost certainly don't want a macro 🙂

seancorfield17:03:46

and flatten is also pretty much always the wrong function to use -- since it flattens everything...

dgb2317:03:05

OK, ill keep the advice about macros in mind! In terms of flatten: what would be a level 1 flatten alternative?

Martin Půda17:03:31

apply concat

🙏 2
seancorfield17:03:47

The other thing to bear in mind is that macro expansion happens "before" evaluation so a macro should generally assume no runtime values. I mean, you can get around that... but you probably shouldn't...

dgb2317:03:18

So what you're saying is that this is a terrible hack? 😄

seancorfield17:03:44

"Just because you could, doesn't mean you should" 🙂

😄 8
seancorfield17:03:43

Macros are surprisingly rare in real-world Clojure. For example, from our work codebase:

Clojure source 568 files 109439 total loc,
    4807 fns, 1134 of which are private,
    659 vars, 44 macros, 103 atoms,

seancorfield17:03:37

We have a few macros that do (static) code gen and a handful that are syntactic sugar for functions.

dgb2317:03:41

(apply concat (seq ...)) works like a charm!

seancorfield17:03:19

You don't need seq there.

dgb2317:03:23

for maps?

seancorfield17:03:58

user=> (apply concat {:a 1 :b 2})
(:a 1 :b 2)

dgb2317:03:12

Right, concat turns it into a seq like map etc. do

seancorfield17:03:22

An alternative is

user=> (mapcat identity {:a 1 :b 2})
(:a 1 :b 2)

seancorfield17:03:29

Yup, I just tend to reserve apply for unrolling function arguments: (apply f a b coll)

dgb2317:03:00

Aha, if you see apply you know what it does quicker

dgb2317:03:11

because its such a general thing

Ben Sless17:03:30

Case will be faster than map lookup, though, especially if all the cases are keywords or ints

👍 2
seancorfield17:03:17

@UK0810AQ2 Yes, but that's a micro-optimization and not worth doing until you have some really performance-sensitive code.

2
dgb2317:03:31

Yeah my idea was to see whether i can get the best of both worlds and benchmark it after

seancorfield17:03:32

Lots of non-idiomatic things can be "faster"...

dgb2317:03:50

Sure, I just did it out of interest

dharrigan17:03:10

I've been writing clojure for about 3 years now and I just wrote my first, very simple macro a few weeks ago.

👍 2
dharrigan17:03:44

and it was only for helping out a test 🙂

👍 2
dgb2317:03:23

Well, my head is spinning when I try to write macros. I only write Clojure on the side unfortunately!

Ben Sless17:03:14

@U04V70XH6 it's one of the things which should be done right first, optimized later. And it should probably not reside in application code, but it can be proper in a library

dgb2317:03:34

My biggest takeaways are: • don't use eval in macros • I still don't understand eval

seancorfield17:03:35

@UK0810AQ2 Perhaps as an illustration, you could show a version of the intended macro that would work, turning a runtime value into a case call?

dgb2317:03:45

but this works... (((eval constantly) :a)) So it's really the macro thing or the combination that i don't udnerstand

kennytilton17:03:56

I am a Common Lisp macro god. Use them all the time in CL. The only time I use them in clojure is for a library I have where the boilerplate would intrude, and offers no value. Clojure being a Lisp-1 solves all the other things for which I used macros in CL. Hth!

👍 2
🎉 2
dgb2317:03:51

this also works: (((eval '{:on-a (constantly :a)}) :on-a)) So there's something the program doesn't know when the macro expands?

Ben Sless17:03:09

@U01EFUL1A8M eval is called on code forms, e.g. something which is quoted. Macros return forms which are then passed to eval anyway, so needing to call eval in a macro is rare

👍 2
dgb2318:03:35

OK I got a rough idea what this macro does. But the mapping itself doesn't live outside of the macro call. What I tried to do is have a runtime value that I can comfortably manipulate turned into a case. It's kind of different no?

Ben Sless18:03:28

run time and compile time is an iffy distinction in a lisp, it's always run time and always compile time

Ben Sless18:03:08

the var is def-ed before the macro, so the state exists at macro expansion time

👍 2
dgb2318:03:13

This works...

(def global-tables (atom {}))

  (swap! global-tables assoc :my-table {:on-a (fn [_] :a) :on-b (fn [_] :b)})

  @global-tables
  
  (defmacro table->case-2 [table-name-kw]
    (let [flattened (mapcat identity (table-name-kw @global-tables))] ; eval sounds scary
      `(fn [k#]
         (case k#
           ~@flattened))))

  (macroexpand-1 '(table->case-2 :my-table))
  ((table->case-2 :my-table) :on-a)
But when I use constantly instead in the table, then it doesnt!

dgb2318:03:57

To add to the confusion, this table also works:

(swap! global-tables assoc :my-table {:on-a (fn [_] ((constantly :a) nil)) :on-b identity})

Martin Půda18:03:13

I think I got that: your macro expands to:

(clojure.core/fn
 [k_24165_auto__]
 (clojure.core/case
  k_24165_auto__
  :on-a
  #object[clojure.core$constantly$fn_5754 0x6b94f7d8 "clojure.core$constantly$fn_5754@6b94f7d8"]
  :on-b
  #object[exercises.core$eval24159$fn_24160 0x2b967189 "exercises.core$eval24159$fn_24160@2b967189"]))
And then, these #object[clojure.core$constantly$fn__5754 0x6b94f7d8 should be evaluated, and that isn't possible. Compare that with the expansion of case:
(macroexpand `(case 1
                1 (constantly :a)))
=>
(let*
 [G__24189 1]
 (case*
  G__24189
  0
  0
  (throw (java.lang.IllegalArgumentException. (clojure.core/str "No matching clause: " G__24189)))
  {1 [1 (clojure.core/constantly :a)]}
  :compact
  :int))
(clojure.core/constantly :a) then evaluates correctly. You would have to use that hash-map as an argument:
(defmacro table->case-2 [table]
  (let [flattened (mapcat identity table)]
    `(fn [k#]
       (case k#
         ~@flattened))))

(macroexpand-1 '(table->case-2 {:on-a (constantly :a) :on-b (fn [_] :b)}))
((table->case-2 {:on-a (constantly :a) :on-b (fn [_] :b)}) :on-a)

ghadi19:03:19

first rule of writing macros: use a fn if possible second rule of writing macros: write the input form and its desired expansion first

🙏 2
2
dgb2310:03:21

Thank you so much for the responses and patience with my questions! I learned a lot from my little macro experiment and from your comments and advice. Some of it triggered me to read the implementations of a few Clojure features and thinking about about higher level concepts and programming in general! Some things I learned: - When (not) to use macros, avoid them when possible. - How to think about quoted forms vs evaluated expressions. - Macros typically want to deal with forms and not with previously evaluated expressions! - How case, cond, condp work and what their rough implications on generated code are. - How multimethods work, they are actually concurrent data structures that maintain several persistent maps for lookup, derive and prefer rules! - Some little things like that I used flatten wrong and when to use (apply concat ...) or mapcat, also that seq is often implied. - Not to worry about trying to optimize things, but still be curious about how things actually work in order to make sensible decisions!

🎉 6
💯 6
teodorlu15:03:48

Great thread! 💯 I wanted to add a transducer alternative to (apply concat {:a 1 :b 2}):

user=> (into [] cat {:a 1 :b 2})
[:a 1 :b 2]
user=> (doc into)
-------------------------
clojure.core/into
([] [to] [to from] [to xform from])
  Returns a new coll consisting of to-coll with all of the items of
  from-coll conjoined. A transducer may be supplied.
Concatenation, but you get to choose the return type. Try swapping [] with () or #{}.

🙏 2
mathpunk19:03:26

There’s two ways I think of macros: 1. An approach to solving the class of problems where you think, “How would I solve this problem myself? I would write code that looked like this.” And because it’s Lisp, you can in fact write a program whose job it is to write code that looks like that. 2.

🙏 2
😆 2
😁 2
💯 2
Ben Sless20:03:59

The first rule of Macro Club is Having Fun (TM)

😁 6
kennytilton21:03:47

The macroexpansion of this is prolly fifty LOC:

(h2 (let [excess (- (mget (fmu :speedometer) :mph) 55)]
          (pp/cl-format nil "The speed is ~8,1f mph ~:[over~;under~] the speed limit."
            (Math/abs excess)  (neg? excess) )))
Hope everyone enjoyed the CL format boolean directive. Had to look it up. I always have to look it up. :rolling_on_the_floor_laughing:

👀 2
dgb2310:03:21

Thank you so much for the responses and patience with my questions! I learned a lot from my little macro experiment and from your comments and advice. Some of it triggered me to read the implementations of a few Clojure features and thinking about about higher level concepts and programming in general! Some things I learned: - When (not) to use macros, avoid them when possible. - How to think about quoted forms vs evaluated expressions. - Macros typically want to deal with forms and not with previously evaluated expressions! - How case, cond, condp work and what their rough implications on generated code are. - How multimethods work, they are actually concurrent data structures that maintain several persistent maps for lookup, derive and prefer rules! - Some little things like that I used flatten wrong and when to use (apply concat ...) or mapcat, also that seq is often implied. - Not to worry about trying to optimize things, but still be curious about how things actually work in order to make sensible decisions!

🎉 6
💯 6