I'd just use a map, possibly even just a vector
Hi all. Iâm not sure whether this is the right forum to ask this question (this is going to be a long post) so please feel free to point me out to the right place.
I was looking for a function to âextractâ values from different places deep inside a map, using something that resembles pattern matching (doesnât need to be pattern matching though). For example suppose you have this map copied from some deps.edn
file:
(def deps {:deps {'acme/spaceship {:mvn/version "1.0.0"}
'org.clojure/clojure {:mvn/version "1.10.1"}}
:aliases {:moon {:extra-deps {'moon/lander {:mvn/version "0.8.7"}
'rock/examiner {:mvn/version "4.0.21"}}
:override-deps {'foo/bar {:mvn/version "1.2.3"}}}
:mars {:extra-deps {'mars/rover {:mvn/version "1.14.9"}
'comms/serial {:mvn/version "2.4.2"}}
:default-deps {'hello/world {:mvn/version "9.8.7"}}}}})
I want a list of all deps, including deps that are deep inside aliases, plus their respective locations (or paths) inside the map. So my first question is: is there such function available somewhere?
I couldnât find such a function, so I made one. Hereâs how it works using the example above. Iâll first define a list of âpatternsâ used to locate the deps:
(def patterns [[:deps]
[:aliases keyword? #{:extra-deps :override-deps :default-deps}]])
Iâll try to explain how those patterns work but I think you probably already get the idea. Here we have two patterns. Patterns are used to match paths to values inside the map, so if a value exists in a path that matches any of the patterns, the value is returned along with its path. Each element in the pattern can be:
⢠A keyword
⢠A value
⢠Or something that implements IFn
If itâs a keyword or something that doesnât implement IFn
, itâs tested for equality. Otherwise itâs called as a predicate function on the mapâs keys.
Now I call the extract
function like this:
(extract patterns deps)
And it returns this list:
({:path [:deps], :val {acme/spaceship {:mvn/version "1.0.0"}, org.clojure/clojure {:mvn/version "1.10.1"}}}
{:path [:aliases :moon :extra-deps], :val {moon/lander {:mvn/version "0.8.7"}, rock/examiner {:mvn/version "4.0.21"}}}
{:path [:aliases :moon :override-deps], :val {:foo/bar {:mvn/version "1.2.3"}}}
{:path [:aliases :mars :extra-deps], :val {mars/rover {:mvn/version "1.14.9"}, comms/serial {:mvn/version "2.4.2"}}}
{:path [:aliases :mars :default-deps], :val {:hello/world {:mvn/version "9.8.7"}}})
Hereâs the implementation:
(defn- key-matches?
[p k]
(if (or (keyword? p) (not (ifn? p)))
(= p k)
(p k)))
(defn- extract-p
[pattern val path]
(let [p (first pattern)
ks (filter #(key-matches? p %) (keys val))]
(if (seq pattern)
(map (fn [k]
(extract-p (rest pattern)
(get val k)
(conj path k)))
ks)
{:path path :val val})))
(defn extract
([patterns m]
(flatten
(map (fn [pattern] (extract-p pattern m [])) patterns))))
Now my second question is, do you see any problems with this implementation, or any opportunities for improvements? I think my biggest concern is how it nests calls to map
, which is why I use flatten
at the end. I wonder if this could result in a significant performance penalty?something like this? https://github.com/DotFox/matchete/blob/main/test/dotfox/matchete/ref_test.cljc#L33-L61
Something like that. Thanks!
This library is still under develop but feel free to have a look at implementation
@delaguardo Very interesting. Iâm trying to understand how the registry works (the first argument passed to m/matcher
).
https://github.com/DotFox/matchete/blob/main/src/dotfox/matchete/compiler.cljc#L287-L292 first argument is a map of identifier -> pattern which you can use later in the pattern given as a second argument for m/matcher wrapped as a [:ref id] sorry for such bloated explanation ) Iâm still working on proper documentation for the lib
default-registry function places ::custom (atom {})
as a container for every custom patterns you might want to bring in as a first argument for m/matcher. And during compilation every pattern like [:ref id]
will use this atom to look up a matcher to use
Take a look at Meander and Specter.
https://github.com/noprompt/meander and https://github.com/redplanetlabs/specter
Both of those are designed to help you navigate deeply nested maps.
Thanks a lot, Iâll take a look!
are there some examples of just⌠really exemplary clojure code that makes a good read?
I donât know. A lot of library code has to do âweirdâ stuff (which is why the library exists), so I suspect most of the âexemplaryâ Clojure code is in proprietary applicationsâŚ
I certainly wouldnât say any of my OSS projects are âexemplaryâ code since they mostly have to jump through hoops to get their jobs done. Iâm not sure Iâd say our code base at work is âexemplaryâ either but some parts of it are nice (and some parts are⌠not so nice) out of 113k linesâŚ
(but then it spans a decade of learning Clojure followed by adopting various idioms over time)
I agree with @seancorfield that it depends on what kind of exemplary you're after, but I will offer an opinion that this library is some of the finest Clojure that I've seen: https://github.com/sicmutils/sicmutils
That does not mean that it is particularly easy to grok, just that it is very well thought out
True, thereâs a big difference between âwell-designedâ and âeasy-to-readâ.
I guess Iâll take a bit of both. Thank you both.
how to fetch the value from Variadic Functionsin clojure
(defn hello [greeting & who]
(println greeting who))
how to get individual values from who?
They're regular sequences, you can use any core function like first
, rest
, nth
, or destructure it further, or map
over it etc.
It's a sequence
You could do something like this: (defn hello [greeting & [firstarg secondarg]] ...)
eg. (doseq [x who] (println x))
(defn hello [greeting & who]
(println greeting (first who)))
ohh got it thanks
@popeyepwr to return a nicely formatted string from a variadic argument, something like this
(defn hello [greeting & who]
(str greeting ", " (clojure.string/join ", " who)))
(hello "Welcome" "fred" "betty" "wilma")
I am pretty sure clojure.string/join
and str
will individually use Java's StringBuilder
internally, but we still end up creating an intermediate string via (clojure.string/join ", " who)
. I wonder if there is one efficient string joining library out there. Something like,
(make-string **somehow represent the complex join logic here**)
(Maybe one should setup everything with interpose
et al. first and then call clojure.string/join
only once in the end.)@hindol.adhya take a look at https://github.com/cgrand/xforms#usage x/str
and x/str!
This is excellent! Thanks.
But this is a different kind of optimisation. Not exactly what I described above.
hi! one question about semantics
(merge {:a 1 :b 2 :c 3}{:a 5 :b 6 :c 7})
=> {:a 5, :b 6, :c 7}
Will it always override first arg's fields with second's ?Yes. For other kinds of merging, merge-with
can be used
How idiomatic would it be to accept required arguments via keywords a-la https://clojure.org/news/2021/03/18/apis-serving-people-and-programs?
depends who you ask :)
I was worried that that might be the response. đ
And if Iâm asking you in particular? đ
even I would say it depends on the situation but generally it's ok by me :)
A thought (inspired by Maybe Not I think): what is required now may become optional in the future... and having the required argument positional would make that more difficult (well, you can always pass nil
). But making a required argument positional does communicate the intent of required-ness more perhaps.
> But making a required argument positional does communicate the intent of required-ness more perhaps. Yep! That was why I was hesitating.
I want to parse an xml file...
(let [input-xml (slurp "JMdict_e")]
(println (parse input-xml))
I'm getting a
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by clojure.lang.InjectedInvoker/0x0000000800b8c840 (/.m2/repository/org/clojure/clojure/1.10.0/clojure-1.10.0.jar) to method com.sun.xml.internal.stream.XMLInputFactoryImpl.createXMLStreamReader(java.io.Reader)
So how can I load a file from the project directory ? o.Ousing clojure.data.xml
@sova That sounds like youâre using clojure.xml
, not clojure.data.xml
. The former is âbuilt inâ to Clojure itself and is deprecated. The latter is org.clojure/data.xml
.
I might use into-edn
instead https://github.com/thebusby/into-edn
(they both have a parse
function so a typo in the :require
would easily get you the old/bad version instead of the new one)
org.clojure/data.xml
is solid.
[clojure.data.xml :refer :all]
ought work?
Caused by: javax.xml.stream.XMLStreamException: ParseError at [row,col]:[1,1]
Message: JAXP00010001: The parser has encountered more than "64000" entity expansions in this document; this is the limit imposed by the JDK.
xDI switched slurp
to <http://java.io|java.io>.FileReader.
and it is working ... đ