Fork me on GitHub
#clojure
<
2023-02-16
>
kennytilton00:02:22

So I was a good little boy and split my O/S library code up into different files/namespaces according to some sensible quality, and everything seems tidy, but...now I am using the library and I have to require a ton of different namespaces. It is not fun. So do I: • get used to it; • get rid of the small namespace/files. Pile all the code into one or two; • create a core NS that has functions of the same name that call the NS function; or • other ______________________. Any advice appreciated! 🙏

Alex Miller (Clojure team)00:02:38

some mixture of #1 and #2

👍 2
Alex Miller (Clojure team)00:02:00

to your degree of taste

Alex Miller (Clojure team)00:02:19

optionality has a lot to do with it imo - if the consumer always needs 3 namespaces to do anything, well maybe that should just be one

pppaul00:02:23

you can also make a consolidation namespace, that aliases forms from other name spaces with different names. sometimes that's a nice way to do things

pppaul00:02:51

(defmacro defalias
  "Defines an alias for qualified source symbol, preserving its metadata (clj only):
    (defalias my-map-alias clojure.core/map)

  Cannot alias Cljs macros.
  Changes to source are not automatically applied to alias."
  ;; TODO Any way to reliably preserve cljs metadata? See #53, commit 2a63a29, etc.

  ([    src      ] `(defalias ~(symbol (name src)) ~src nil))
  ([sym src      ] `(defalias ~sym                 ~src nil))
  ([sym src attrs]
   (let [attrs (if (string? attrs) {:doc attrs} attrs)] ; Back compatibility
     `(let [sym-meta# (meta (var ~src))
            ;;(select-keys (meta (var ~src)) [:doc :arglists :private :macro])
            attrs# (merge sym-meta# ~attrs)]
        (alter-meta! (def ~sym @(var ~src)) merge attrs#)
        (var ~sym)))))

kennytilton00:02:13

"consolidation namespace" I like it! The term, anyway. Sadly, my API is 80% macrology. Thx also for defalias! 🙏

Alex Miller (Clojure team)00:02:16

In my opinion, all that aliasing junk is a mess. If you want it all in one namespace, put it all in one namespace in the first place. You can still split it into files if that helps you manage it on your side and use load-file (which is how clojure.core is defined)

10
kennytilton00:02:11

"load-file"? Sounds promising. 🤞

pppaul00:02:04

i agree with alex, however sometimes it's convenient to have namespaces that use the same symbols, and then rename them in the consolidation namespace

Alex Miller (Clojure team)00:02:55

I remain unconvinced :)

pppaul00:02:41

i think it's pretty rare, and i probably did this in one of my code bases without knowing better

kennytilton00:02:51

The doc on load is a little thin, or at least the doc I found. The question is, what NS gets assigned to what? I gather from the clojure.core example...well, it must resolve to that. But does that mean the loaded NS name is ignored? I cannot imagine the NS is left blank. I'll do some experimentation. 🔬 Thx for a good lead! Seems ideal.

Alex Miller (Clojure team)01:02:33

it is loading in the current namespace

Alex Miller (Clojure team)01:02:05

because it does not call ns

Alex Miller (Clojure team)01:02:38

namespaces do not have to match 1-1 with files

Alex Miller (Clojure team)01:02:47

require finds a namespace to load by mapping the namespace to a file name, then the ns macro at the top is what creates and sets the namespace context for the subsequent load

🙏 2
💡 2
practicalli-johnny01:02:40

Other... I typically thing of namespaces in a project as a tree (in winter, so there are only branches and no leaves). The main namespace is the trunk of the tree with a few major branches. Each branch may have a small number of branches. This minimises the requires in each namespace. I will start with one namespace and divide that into sections using line comments (using a snippet). Creating these sections adds to the readability and discover-ability of the overall namespace. Sectioning also helps identify which parts of the namespace may be useful to section off into their own namespace. I try avoid breaking up into namespaces across the tree as this creates complexity. The exception being a specification namespace which would be defined as a single namespace at the top level anyway. I try focus how valuable the logical separation into separate files is, mainly for readability and comprehension of what the code is trying to achieve. This keeps the projects pretty flat and easy to refactor.

🙏 2
2
didibus06:02:52

load doesn't work well with tooling unfortunately, I like it otherwise.

didibus06:02:32

In your case, you could just make a helper that requires them all. Well, ok, that'll also not work well with tooling probably :white_frowning_face:

didibus06:02:47

So maybe you want... Copy/Paste ?

vemv09:02:25

your IDE should place the requires for you. Similarly if you use an aliasing convention, you can infer that foo stands for myproject.foo. So aliasing shouldn't have a cognitive cost. With clj-refactor latest, if you hit foo/ anywhere in your code, [myproject.foo :as foo] will be automatically inserted in the ns form, even if foo wasn't used as an alias anywhere in the project. It's magic! clojure-lsp also has similar functionality I believe.

kennytilton10:02:44

Thx, @U45T93RA6, but I should have emphasized that the context here is not wanting to bedevil users of my library with excessive requires. I mean, it annoys me, too, which is why it concerns me, but even with IDE conveniences, it seems like sth I should cure.

vemv10:02:28

Yeah it's a delicate topic, and also one there will never be a definitive truth for. One could argue that consumers can also enjoy a non-monolithic API, for conceptual reasons. If those concepts (namespaces) make sense on their own, you're giving consumers a chance to learn about your project one bite-sized chunk at a time. And to disregard concepts that might be irrelevant for their use case. It also seems perfectly possible do to both approaches at the same time, i.e. separate namespaces + one opt-in namespace merging all those.

didibus20:02:07

Put the require on your readme, users can copy/paste it into their ns 😛

didibus20:02:40

But for libraries like that, normally I do what Alex said. Once I'm ready to release, I reconsider if certain ns should be merged together or not to improve ergonomics. If I still want more segregation on my side on the implementation, I create like a "Facade" NS, and I expose functions that call others underneath. Or I do something like: (def fn other/fn) And also copy the meta of other/fn to fn so it has the same doc and all. Doing that for macros is trickier, there's still a way to do it though, but I already forgot what it is. Maybe that's similar to the defalias? Though I would not call this aliasing really, I'm declaring new Vars and setting their function in them.

Rupert (All Street)11:02:57

Having to require more namespaces is a bit of extra work for the library user but it can give more optionality to the library maker (e.g. a namespace becomes it's own dedicated library without breaking backwards compatibility). So as a library user I don't mind having to require in a bunch of namespaces if the trade off is the library has more flexibile and future proof. One approach I take when creating a new namespace is pasting in a bunch of require statements, writing the code then having the code linter to tell me which require statements I can delete at a later date. So going to your question original question, I would lean towards (A) "get used to it" and (B) "get rid of the small namespaces/files" - if you really don't need the flexibility after all.

🙏 2
practicalli-johnny00:02:20

Assuming I've created a practicalli.service.utils namespace with a couple of functions and they were evaluated in the repl. Then decide I didn't actually need the namespace or functions. What happens when I use https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/remove-ns to remove the namespace? Are the function definitions in that namespace removed from the REPL, or do they remain even though I assume they are unreachable? Assuming I wasnt using clojure.tools.namespace.repl/refresh, would I first remove each function definition from the REPL via ns-unmap and then use remove-ns to remove the (now hopefully empty) namespace.

hiredman01:02:59

there is a map of namespace names to namespace objects (which themselves are a sort of map) remove-ns just removes the entry for the named namespace from that map

hiredman01:02:30

I am sure at some point in over a decade of using clojure I've called remove-ns, but I cannot remember it. hard to see the point.

hiredman01:02:22

it doesn't even touch the state that require keeps to avoid double loading, so if you require a namespace, then remove-ns it, then require it again it won't load the second time

seancorfield01:02:53

Yeah, I tried using remove-ns for a while and was quite puzzled by its apparent behavior -- hiredman's comments make sense of my puzzlement (and a couple of tests in the REPL verify he is correct). Here's what I use nowadays in the occasional situation that I want to "clean up" a namespace in-memory and then re-eval (parts of) it: https://github.com/seancorfield/vscode-calva-setup/blob/develop/calva/config.edn#L23-L39

Bart Kleijngeld08:02:44

I want to learn more about parsing data, grammars, schemas, coercing, transforming, term rewriting, just to name a few keywords. I would like to understand the foundation better so I can use libraries like Malli and Meander to work with models of all sorts of shapes and be able to parse them meaningfully and then transform them powerfully in a declarative and scalable way. I understand if this is a bit of a broad request, but I'm looking for any book recommendations that come to mind 🙂 Edit: it does not have to be a Clojure book.

simongray09:02:42

The Joy of Clojure has an interesting chapter on unification.

gratitude 2
Bart Kleijngeld12:02:19

Very nice 🙂! These do sound a little advanced for my level right now though

Ben Sless12:02:36

I'm not going to pretend I read all of them, but you asked and I poked Joel's head for the details some time ago, so might as well keep them at hand I also found those books tend to look insurmountable but if you just pop one open and begin it Just Works (TM)

Bart Kleijngeld13:02:30

Definitely useful indeed. Interesting books to read for years 😉

pppaul18:02:57

I found instaparse an easy way to get into making grammars

rutledgepaulv19:02:46

I'd recommend any of the books by Terence Parr too.

Bart Kleijngeld21:02:54

Thanks all 🙂. Are these books mostly about parsing string-based languages? Any tips maybe on languages that are defined in other structures, most notably graphs or hash maps?

Drew Verlee19:02:44

Why is (+ nil ) an error and not 0 in clj like it is in cljs. I would like to think of nil like the emptyset and behavior doesnt fit. My guess is, it's because the host does it that way.

seancorfield19:02:17

(~/clojure)-(!2007)-> node
> + null
0
>
"Thanks, JS!"

🙈 2
seancorfield19:02:58

There are a lot of cases -- in both clj and cljs -- where the behavior is "undefined" and so you get whatever the underlying host platform does.

dpsutton19:02:17

but thinking of nil as the empty set doesn’t seem to help much here. (+ #{}) doesn’t sound very well formed either

seancorfield19:02:50

Clojure 1.11.1
user=> (+ "x")
Execution error (ClassCastException) at java.lang.Class/cast (Class.java:3946).
Cannot cast java.lang.String to java.lang.Number
user=>
but
> + "x"
NaN
>
(and Clojure certainly has a NaN)

p-himik19:02:22

I'm guessing that there's an issue in type inference in CLJS - you should've been given a similar warning for (+ nil):

cljs.user=> (+ "a")
WARNING: cljs.core/+, all arguments must be numbers, got [string] instead at line 1 <cljs repl>
"a"

p-himik19:02:50

Right - here's a part of the implementation, note the TODO:

(defn numeric-type?
  #?(:cljs {:tag boolean})
  [t]
  ;; TODO: type inference is not strong enough to detect that
  ;; when functions like first won't return nil, so variadic
  ;; numeric functions like cljs.core/< would produce a spurious
  ;; warning without this - David
  (cond
    (nil? t) true
    (= 'clj-nil t) true
    (js-tag? t) true ;; TODO: revisit
    :else
    (if (and (symbol? t) (some? (get NUMERIC_SET t)))
      true
      (when #?(:clj  (set? t)
               :cljs (impl/cljs-set? t))
        (or (contains? t 'number)
            (contains? t 'long)
            (contains? t 'double)
            (contains? t 'any)
            (contains? t 'js))))))

🤯 2
👀 2
seancorfield20:02:15

We just had another thread saying (int \8) differed across clj/s and I bet that's another weird hosted difference that is only partly papered over by clj/s.

jpmonettas19:02:56

I'm hitting a case where I can eval a form but I can't eval the macroexpansion of the form :

(require '[com.rpl.specter :as sp])

;; pasting this at the repl works just fine
(let [n 0]
    (sp/select [:b n] {:b [{1 2} {2 4}]}))

;; but pasting the output of the macroexpansion doesn't
(clojure.walk/macroexpand-all
 '(let [n 0]
    (sp/select [:b n] {:b [{1 2} {2 4}]})))
looking at the macroexpansion make sense it should't work since it is refering to (var n) which doesn't exist (it is a local binding, not a global var) what am I missing?

hiredman19:02:44

macroexpand-all isn't entirely identical to what the compiler does

hiredman19:02:41

It sounds like you aren't just doing the expansion differently, but are also introducing serialization through the repl printing out the expansion and deserialization when you paste it back into the repl

hiredman19:02:49

Which would be my bet

jpmonettas19:02:17

I don't think it is related to serialization since you get the same issue when you do

(def form
  (clojure.walk/macroexpand-all
  '(let [n 0]
     (sp/select [:b n] {:b [{1 2} {2 4}]}))))

(eval form)

jeaye20:02:32

Any clean way to match a range (which is too big to write out in code)? Looking to do something like:

(case foo
  (0x8000 => 0x9fff) ...
  0xff40 ...
  ...)
Surely I can cond here, but was curious if I can do better. core.match doesn't seem to have anything for this either.

Vishal Gautam20:02:02

Something like this?

(def vals {0x8000 0x9fff ...});
(get vals foo)

p-himik20:02:47

I don't think there's anything better than cond along with the fact that you can pass multiple arguments to the comparison functions.

jeaye20:02:00

No, that's mapping 0x8000 to 0x9fff. I'm looking for anything within the range of 0x8000 and 0x9fff to be matched.

hiredman20:02:11

the size of the code that is generated for case is proportional to the number of values to test against

jeaye20:02:36

Ah, so probably wouldn't want it with case anyway then.

pppaul20:02:01

what is a range? i'm not really sure why you are using octals (is 0x octal vals?)

jeaye20:02:14

0x is hex, not octal.

dpsutton20:02:23

condp and a decent function would make this very elegant i think

pppaul20:02:51

is it ok to use decimal for this example? can i make a range as [0 1] is that good enough?

jpmonettas20:02:10

(case x
  1 :a
  10 :b)
decompiles to :
             tableswitch {
             2: 76
             3: 108
             4: 108
             5: 108
             6: 108
             7: 108
             8: 108
             9: 108
             10: 108
             11: 92
             default: 108
             }
which is interesting since there is already kind of a range thing going on there

jpmonettas20:02:21

not that it answers your question, just interesting

pppaul20:02:45

you can use core.match with guards to match on a range if you represent it as a vector

jeaye20:02:08

@U0LAJQLQ1 Base 10 or 16 doesn't change anything here. Same question applies. 🙂

pppaul20:02:18

but, how do you want to do dispatch on a range, exactly? core.match to me sounds like you are interested in partial matching

pppaul20:02:53

do you want to dispatch on something that falls in your range (another range/point)

jeaye20:02:04

The equivalent of:

(cond
  (and (<= 0x8000 addr) (<= addr 0x9fff)) ...
  (= 0xff40 addr) ...
  :else ...)

pppaul20:02:35

core.match is good for that. let me come up with some code, give me a min

dpsutton20:02:43

qp=> (let [between? (fn [[low high] x] (<= low x high))]
       (condp between? 17
         [0 10] :first
         [11 20] :second
         [21 30]))
:second

😎 2
jeaye20:02:40

@U11BV7MTK Hm. With that, I'd need to have the fn handle single values, too, for the 0xff40 case.

jeaye20:02:50

Ends up being worse than cond, I think.

jeaye20:02:14

🤷 Was worth checking. Thanks for all the replies.

pppaul20:02:11

(let [target-range-start 0
      target-range-end   5
      test-point         [1 1]
      test-range         [1 5]]
  (clojure.core.match/match
    test-range
    [target-range-start target-range-end] :exact-match
    [(_ :guard #(>= % target-range-start)) (_ :guard #(<= % target-range-end))] :between    
    :else :no-match))

jeaye20:02:22

I'm not looking for a clamp function, Vishal. 🙂 Thanks, though.

😅 2
jeaye20:02:12

Hm, thanks for sharing that, @U0LAJQLQ1. Match guards are neat.

pppaul20:02:28

i feel they get in the way of the pattern, too many of them and pattern matching fails at what it's good at (documentation)

jeaye20:02:07

Yeah, it ends up being very indirect.

pppaul20:02:39

i think a nice way to do this is to make a function that output something that a case statement can use (keywords). then the case can dispatch on something that is easy to read like the things i returned in the pattern matching

jeaye20:02:07

Mm, I know what you mean.

pppaul20:02:33

yeah, that's close to what i mean. i guess i like making the documentation more a part of the code

jeaye20:02:23

Yeah, I know what you mean. Had cooked this up prior to what you said.

p-himik21:02:37

You can put all the constant comparison in a case and the cond part for ranges into its default branch. Something like

(case addr
  0xff40 0
  0xff41 0
  (0xff42 0xff43 0xff44) 0  ;; Note that you can also group constants.
  ; else
  (cond
    (<= 0x8000 addr 0x9fff)
    (.getUint8 (-> emu :gpu :vram) addr)
    ...))

jeaye21:02:18

Ah, that's a good idea. Combination of case and cond.

jpmonettas22:02:53

I think it also depends on the order of the clauses, since a cond can also be faster than a case https://insideclojure.org/2015/04/27/poly-perf/

jeaye22:02:44

Ohh, very nice. Perf matters here, so I'll tinker with this.

Ben Sless22:02:20

If perf matters make sure to use the 2-arity of <= and not 3-arity

👍 2
respatialized21:02:05

I am using deftype to implement an instance of a protocol, and it might be handy to have instances of that type hold on to some discrete bits of internal state. Is there any drawback to using an ordinary map internally to represent this mutable state? I intend to keep this state fully private and internal to instances of this type.