This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-03-02
Channels
- # announcements (25)
- # babashka (76)
- # beginners (74)
- # biff (36)
- # calva (11)
- # cider (5)
- # clerk (43)
- # cljs-dev (4)
- # cljsrn (12)
- # clojure (111)
- # clojure-austin (14)
- # clojure-europe (82)
- # clojure-nl (2)
- # clojure-norway (5)
- # clojure-uk (1)
- # clojurescript (36)
- # core-async (13)
- # cursive (30)
- # datomic (12)
- # fulcro (6)
- # honeysql (9)
- # hyperfiddle (73)
- # instaparse (3)
- # introduce-yourself (1)
- # membrane (40)
- # nbb (2)
- # off-topic (6)
- # other-languages (9)
- # polylith (33)
- # reagent (2)
- # reitit (7)
- # rum (7)
- # shadow-cljs (47)
- # tools-deps (10)
- # vim (11)
- # xtdb (16)
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?
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))))
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
both max
and max-key
work with Numbers only
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?
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.
Ok thanks, will look. It’s not a huge deal - I was just wondering if I was missing something “simple”.
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"...
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/
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
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
> 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!
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
I don't think you even need a macro for this -- you can do it with a regular function, yes?
But how do I generate a function that turns a map (like the ones above) into a case
... ?
You have a hash map -- why do you need a case
there?
I don't need it. I wanted to try to generate a function from a map that does static dispatch.
whether that's a good idea, I don't know. But the error message is very confusing regardless!
(case k :kw1 fn1 :kw2 fn2)
is still a dynamic "lookup"...
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 🙂
and flatten
is also pretty much always the wrong function to use -- since it flattens everything...
OK, ill keep the advice about macros in mind! In terms of flatten: what would be a level 1 flatten alternative?
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...
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,
We have a few macros that do (static) code gen and a handful that are syntactic sugar for functions.
You don't need seq
there.
user=> (apply concat {:a 1 :b 2})
(:a 1 :b 2)
An alternative is
user=> (mapcat identity {:a 1 :b 2})
(:a 1 :b 2)
https://github.com/clojure/clojure/blob/clojure-1.10.1/src/clj/clojure/core.clj#L2783 Seems to be the same-ish!
Yup, I just tend to reserve apply
for unrolling function arguments: (apply f a b coll)
Case will be faster than map lookup, though, especially if all the cases are keywords or ints
@UK0810AQ2 Yes, but that's a micro-optimization and not worth doing until you have some really performance-sensitive code.
Yeah my idea was to see whether i can get the best of both worlds and benchmark it after
Lots of non-idiomatic things can be "faster"...
I've been writing clojure for about 3 years now and I just wrote my first, very simple macro a few weeks ago.
Well, my head is spinning when I try to write macros. I only write Clojure on the side unfortunately!
@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
@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?
but this works...
(((eval constantly) :a))
So it's really the macro thing or the combination that i don't udnerstand
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!
this also works:
(((eval '{:on-a (constantly :a)}) :on-a))
So there's something the program doesn't know when the macro expands?
@U04V70XH6 https://github.com/phronmophobic/shape-bench/blob/main/src/shapes.clj#L219
@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
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?
run time and compile time is an iffy distinction in a lisp, it's always run time and always compile time
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!To add to the confusion, this table also works:
(swap! global-tables assoc :my-table {:on-a (fn [_] ((constantly :a) nil)) :on-b identity})
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)
first rule of writing macros: use a fn if possible second rule of writing macros: write the input form and its desired expansion first
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!
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 #{}
.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.
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: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!