Fork me on GitHub
#beginners
<
2022-11-03
>
Matthew Twomey02:11:29

Pattern question: I have a lib of functions which interact with a client. Most of these functions have the pattern of: get-client; do-stuff using the client. In other non Clojure environments, I would be likely to create a getClient function that returns the existing client or if one doesn’t exist, creates and returns it (thereby reusing the client). I am having trouble “thinking in Clojure” for this, what are some ways I can achieve similar client reuse?

walterl02:11:59

In general we tend to pass things like clients in as function arguments. So you may have (defn do-stuff [client ,,,] ,,,). This keeps your function pure and allows you to test it very easily.

walterl02:11:29

Dependency injection libraries like https://github.com/stuartsierra/component, https://github.com/weavejester/integrant and https://github.com/weavejester/integrant can help you manage the starting/stopping and cross-referencing of stateful components like that.

walterl02:11:01

But in some cases a bit of global state (with all its attendant problems) could be sufficient. For such cases I like https://github.com/metabase/toucan/blob/master/src/toucan/db.clj#L40-L48 kind of pattern.

dorab02:11:34

If I understand your question correctly, the following code might help

user> (def get-client
        (let [myclient (atom nil)]
          (fn [] (if @myclient
                   (do (println "reusing") @myclient)
                   (do (println "creating") (reset! myclient 42) @myclient)))))
                     
#'user/get-client
user> (get-client)
creating
42
user> (get-client)
reusing
42
user> 

walterl02:11:35

I.e. prefer a binding, and fall back to an atom.

Matthew Twomey02:11:31

Thanks both @UJY23QLS1 and @U0AT6MBUL for giving me a couple of options to think on here. I can see how I can apply these. I think I’ll ponder for a bit before I settle on an approach. One side-note, I realized I could “keep the function pure” by always passing the client along with the request, but I don’t want the users of the lib to have to deal with (or even know about) the client aspect of it.

Matthew Twomey02:11:33

I also see that these libraries you linked @UJY23QLS1, are aimed squarely at this situation. So I’m going to think about if I want / need to go that route of it it’s overkill for my fairly simple use case.

walterl02:11:10

Yeah, it probably won't be worth it for simple cases.

walterl02:11:17

Still, initializing some library state, passing it back to the caller, and expecting that state as a function argument, is quite a common pattern. E.g. https://github.com/seancorfield/next-jdbc#usage

Matthew Twomey02:11:14

Yeah, that I do realize. I’ve done that myself often over the years. I just dislike doing it in cases where there is no “other” reason for the caller to deal with the client - besides avoiding caching the client on the “backend”. It feel like I’m just dumping the problem onto the caller.

robertfw03:11:58

instead of seeing it as dumping the problem onto the caller, you can view it as giving the caller more explicit control over state management, which I think anyone consuming a Clojure library will appreciate.

☝️ 4
Matthew Twomey04:11:25

Fair enough @UG00LE5TN, I’ll think on that.

pithyless13:11:33

Two common approaches are (foo client foo-args) and (foo foo-args {:client client :extra :opts}) - I think if the client state is something that is secondary to the problem and will unlikely be modified or need to considered by the API user, then perhaps the second approach is more appropriate (since the opts map is optional). Then the library could def or memoize a default client that is used if none is passed in. Two other approaches that I am not a fan of is multi-arity functions (with and without extra client arg); and dynamic vars. It's mostly a question of API aesthetics, but e.g. I would consider the first-arg approach for a database connection (a client API must know about and setup a DB conn) and the optional-map approach more for an overridable threadpool in an HTTP client libary. Also remember that it would be good if the convention is consistent for all functions in the library: they should all take the client as the first argument (e.g. a datasource in next.jdbc) or as the last argument in all variadic functions (e.g. the optional :registry key in all malli functions).

mandudeman05:11:51

I am trying to follow the "Quickstart" set up of XTDB which can be found in this url: https://docs.xtdb.com/guides/quickstart/. As I followed the steps, I tried to start the project repl and connect in VS Code using Calva. When I did, the terminal threw this error:

mandudeman05:11:14

# A fatal error has been detected by the Java Runtime Environment: # # SIGSEGV (0xb) at pc=0x00007f212f299073, pid=11126, tid=11178 # # JRE version: Java(TM) SE Runtime Environment (19.0.1+10) (build 19.0.1+10-21) # Java VM: Java HotSpot(TM) 64-Bit Server VM (19.0.1+10-21, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, linux-amd64) # Problematic frame: # C [librocksdbjni-linux64.so+0x299073] Java_org_rocksdb_RocksDB_getSnapshot+0x3 # # Core dump will be written. Default location: Core dumps may be processed with "/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -- %E" (or dumping to /home/user/dev-clojure/xtquickstart/core.11126) # # An error report file with more information is saved as: # /home/user/dev-clojure/xtquickstart/hs_err_pid11126.log # # If you would like to submit a bug report, please visit: # https://bugreport.java.com/bugreport/crash.jsp # The crash happened outside the Java Virtual Machine in native code. # See problematic frame for where to report the bug. # Aborted (core dumped) Jack-in process exited. Status: 134

phronmophobic05:11:13

you can also ask in the #CG3AM2F7V channel. It looks like the problem might be that you're on a linux-amd64 and using a configuration for linux64.

mandudeman06:11:28

Thanks a lot for the reply. I am going to reach out to the channel and convey to them my question.

mandudeman07:11:53

"It looks like the problem might be that you're on a linux-amd64 and using a configuration for linux64." If this is really the cause of the error, do you have any idea on how am I going to fix this? Do I need to re-install something?

mandudeman07:11:00

"It looks like the problem might be that you're on a linux-amd64 and using a configuration for linux64." What exactly do you mean by your reply? Does it have to do with my Java SDK installation?

phronmophobic07:11:43

it doesn't look like rocksdb has a native lib for linux-amd64

phronmophobic07:11:48

you can try using one of the other storage options, https://docs.xtdb.com/storage/

mandudeman07:11:31

Does this mean that I can use XTDB on linux if RocksDB is not going to work on it?

mandudeman07:11:13

Ok, I will try jdbc. Thanks.

mandudeman08:11:40

Thanks a lot. It now works!

🎉 1
mandudeman05:11:03

May I know why this error came out? If possible, kindly lead me to the direction where I can fix this problem. Thank you very much!

Valentin Mouret08:11:21

Hello 🙂 I am finally able to work in Clojure and I finding it very pleasant. It’s especially satisfying when walking back on problems and solving them the Clojure way™. Somehow, this leads me to structurally repetitive code, and that sounds like a queue for macros. https://www.braveclojure.com/writing-macros/ has proven really helpful already to understand how to technically write one, but I still struggle to see how to apply them in the real world. Do you have examples in the wild that could be helpful for a beginner?

Valentin Mouret09:11:22

For example, I am trying to wrap https://github.com/yogthos/migratus to make it a bit simpler to use. Its migrate and rollback functions take a config as a first argument. In my case, I currently have two environments: dev and prod, and I would like to default to dev. So, I wrapped them like so:

(defn- get-config
  ([]    (get-config ::postgres/dev))
  ([env] (update-in base-config [:db]
                    merge (env postgres/configs))))

(defn migrate
  ([]    (migrate ::postgres/dev))
  ([env] (-> env get-config migratus/migrate)))

(defn rollback
  ([]    (rollback ::postgres/dev))
  ([env] (-> env get-config migratus/rollback)))
And here is the structural repetition. rollback and migrate carry very little information, 95% is repetition. I feel like macros would help me here, but maybe I am mistaken.

lsenjov09:11:28

Think of macros as functions around your code. Can you see where you could plug in parameters for each?

lsenjov09:11:25

Although note that clojure generally discourages macros, as they add extra hidden complexity to code.

lsenjov09:11:44

With that in mind, what would you like your macro api to look like? (wrap-migratus "dev")?

Valentin Mouret09:11:45

I was rather thinking about defining a macro to write 2-arity functions, such that I can write: (defoperation rollback migratus/rollback) Maybe that’s a bad idea, but that’s what instinctively comes to me when I see the above code. Maybe there are more idiomatic ways of dealing with the problem.

lsenjov09:11:29

Sure, why not? That works well enough. Have you tried writing a macro yourself?

Valentin Mouret09:11:08

Well, I did but failed miserably. They are a bit tedious to write as well, the errors are not very clear for a beginner like me. With my current knowledge, it would take too much time to write it, and it would not be worth it in my opinion. That’s why I am inquiring about good examples in the wild, so that I can learn from good examples.

lsenjov09:11:18

Oh right. Well I'm on the train home and it looked like fun, so

(ns scratch)

(defmacro defoperation
  [op-name op-reference]
  `(clojure.core/defn ~op-name
     ([]    (~op-name :ns.postgres/dev))
     ([env] (clojure.core/-> env get-config ~op-reference))))
(macroexpand-1
  '(defoperation rollback migratus/rollback))
;; => result
(clojure.core/defn
 rollback
 ([] (rollback :ns.postgres/dev))
 ([scratch/env]
  (clojure.core/-> scratch/env scratch/get-config migratus/rollback)))

1
lsenjov09:11:31

Of note: it's probably a good idea to not rely on relative namespaces with macros

lsenjov09:11:39

Actually that's probably a small error in there, one moment

Valentin Mouret09:11:42

I was going to ask the question. 😄

lsenjov09:11:28

So things to note: because I didn't give env a namespace, it evals to the current namespace because of the backtick at the beginning

lsenjov09:11:02

I specifically removed the :: from postgres/dev to something with an absolute namespace for the same sort of reason

Valentin Mouret09:11:02

Why macroexpand-1? I think I had a similar macro (delete it since :face_palm: ), but your macro also fails with macroexpand. It works with macroexpand-1.

lsenjov09:11:22

So macroexpand will then expand every other macro below it

💡 1
lsenjov09:11:30

macroexpand-1 only does the top level macro

lsenjov09:11:49

So you can do (-> form macroexpand-1 macroexpand-1) to do two macro expansions

Valentin Mouret09:11:59

That makes a lot of sense.

Valentin Mouret09:11:28

Thanks a lot for you time and clear explanations. 🙇

❤️ 1
lsenjov09:11:10

Note that I'm middling at best with macros. If you want to go heavy macros I'd recommend reading SICP

lsenjov09:11:46

There's a lot of gotchas with macros and they're hard to reason about, so again it's generally recommended to avoid them in clojure without a very good reason

lsenjov09:11:52

And probably not when it's only a couple of cases for your pattern. Probably more like when you have half a dozen items of very hairy syntax repeated the same way over and over

lsenjov09:11:56

One good example (closed code but I can explain what it's doing) is importing a bunch of react components into reagent

lsenjov09:11:46

So there's a bunch of slightly hairy stuff around looking up a name in a js list of objects, then defing each one with adapt-react-component

lsenjov09:11:01

About 3-4 lines per entry. Not terrible but still painful when pulling in 30+ objects

lsenjov09:11:30

So the declaration is instead something like (import-raws [Button Card Overlay ...])

lsenjov09:11:46

And each item is now def'd in that namespace for import elsewhere

Valentin Mouret09:11:16

That makes sense. I agree that it’s overkill for my case, it felt like a good experiment though. Which is not over because the macro is not actually working 😄. macroexpand-1 is working, but macroexpand is not. It’s saying it does not conform to defn’s spec.

Valentin Mouret09:11:20

1. Caused by clojure.lang.ExceptionInfo
   Call to clojure.core/defn did not conform to spec.
   #:clojure.spec.alpha{:args
                        (migrate
                         ([] (migrate :me.postgres/dev))
                         ([me.migrations/env]
                          (clojure.core/->
                           me.migrations/env
                           me.migrations/get-config
                           migratus/migrate)))}

Valentin Mouret09:11:38

(`me` is the root namespace of my project)

lsenjov09:11:21

Oh I see. me.migrations/env is throwing the error, can't be namespaced

lsenjov09:11:18

(defmacro defoperation
  [op-name op-reference]
  `(clojure.core/defn ~op-name
     ([]    (~op-name :ns.postgres/dev))
     ([~'env] (clojure.core/-> ~'env get-config ~op-reference))))
;; =>
(clojure.core/defn
 rollback
 ([] (rollback :ns.postgres/dev))
 ([env] (clojure.core/-> env scratch/get-config migratus/rollback)))

💡 1
lsenjov09:11:04

So ~ is evaluate, so ~'env is evaluate (quote env), which makes it just the symbol env

lsenjov09:11:19

(Well it's not quite evaluate, but it'll do for our understanding)

Valentin Mouret09:11:08

Now I understand the juggling with chainsaw analogy. 😄

lsenjov09:11:32

Yeah.. as a learning exercise absolutely do some more, but avoid in codebases where possible

Valentin Mouret09:11:38

Thanks for the solution and cautionary tale, it now works like a charm and will I avoid using it. 😄

😂 1
lsenjov09:11:23

Again, if you want to dive more into the wonderful world of macros (or just get an idea of what you can really do with them), have a read through SICP

lsenjov09:11:52

Also so you can learn how to make hygienic macros

Valentin Mouret10:11:20

It feels like a threshold along the journey. Write macros because you think you need them, get bitten hard, stop using macros, carefully write macros. Like this meme with the gaussian curve.

lsenjov10:11:25

I dunno, I'm pretty sure I'm not even at the peak of the curve, and I feel like after the peak there's the racketeers

lsenjov10:11:48

And their brains are just the next level aliens I'm nowhere near yet

lsenjov10:11:33

(I still like Clojure more than racket for sheer pragmatism and getting stuff done, but good god do they have some weird and cool stuff)

👍 1
☝️ 1
Benjamin12:11:34

Jo is there a way to write a link to a file (relative to the project root, the default dir when running a dev repl) in a doc string? If I do [[]] cider tries to interpret the string as a symbol. I realized a better way is to link to a perma link of e.g. the github file - so I guess I answered my question

respatialized14:11:25

is there a function to extract the namespace (as a keyword or symbol) of a fully-qualified keyword?

Alex Miller (Clojure team)14:11:03

that will give you a string, which you can pass to symbol or keyword

1
respatialized14:11:11

that's what I get for trying to trust google instead of just looking back over https://clojure.github.io/clojure/clojure.core-api.html thanks!

andy.fingerhut14:11:32

You may also find one of the searchable cheastsheets here useful: https://jafingerhut.github.io/

andy.fingerhut14:11:50

Or in a REPL, the apropos and find-doc functions.

Matthew Twomey15:11:02

I hadn’t seen this cheatsheet, awesome

andy.fingerhut17:11:14

The official version on http://clojure.org has everything except the dynamic search text box, but you can use browser text search on the page to look for strings, too: https://clojure.org/api/cheatsheet. That page also has a link to the one that I gave above.

mbjarland15:11:35

I have a question which I though I for sure would have figured out by now. Assume I have a protocol FooAbility with methods a and b defined in namespace one. Assume further that I want to reify and use an instance implementing the protocol in namespace two. Is the correct way to require '[one :as p]) in namespace two and then have to prefix (p/a instance args) in all references to the protocol itself and the methods in two?

mbjarland15:11:22

due to requires cycles I refactored out some protocols and instead of using them from current namespace I now have them in a separate one and realized I don't quite grok the above. It seems you can not "reuquire the protocol" but that the methods are entities in themselves and if I wanted to avoid namespace prefixing (yeah I know you probably should) I would have to refer both the protocol and the protocol methods?

Alex Miller (Clojure team)16:11:26

protocol methods are functions in the namespace where the protocol is defined, and you should treat them that way for calling purposes

mbjarland16:11:56

ok makes sense from my poking at the repl

Alex Miller (Clojure team)16:11:24

that means requiring the namespace defining the protocol and using function names that sufficiently refer to that (either by fully qualified name, aliased name, or by referring them to use as short name)

mbjarland16:11:46

in fact that makes things simpler

mbjarland16:11:52

excellent, thank you!

Alex Miller (Clojure team)16:11:20

an important thing here is that functions are an abstraction and from the caller perspective, whether you are calling a function, a macro, a multimethod, a protocol method, a var that holds a map, etc - the caller says exactly the same thing (f args)

Alex Miller (Clojure team)16:11:48

which means the implementor is free to change the implementation without affecting the caller

💡 1
mbjarland16:11:56

with that understanding I guess this should have been obvious

mbjarland16:11:04

thanks again - this makes total sense

Jared Langson19:11:13

How do I dynamically construct regex? I have a function like

(defn my-funct [string character]
  (let [pattern (re-pattern (str "#" character))]
do stuff))
I want to take a single character and turn it into a regex. How do I do that?

teodorlu19:11:51

i'm not quite following. Do you have an example of how you want your function to behave? I was about to suggest looking into re-pattern, but I see that you've already found it.

teodorlu19:11:03

(re-find (re-pattern "#.") "abc#def")
;; => "#d"
(re-find (re-pattern ".#.") "abc#def")
;; => "c#d"

hiredman19:11:56

The # prefix to regexes just tells the reader to call re-pattern on the following string

☝️ 1
hiredman19:11:58

Your issue is likely the fact that you are prepending # onto the string of your regex to mimic the syntax of regex literals before passing to re-pattern

👍 1
teodorlu19:11:00

;; equivalent:
(re-find (re-pattern "#.") "abc#def")
(re-find #"#." "abc#def")

👍 1
Francesco Pischedda19:11:12

A story about defmulti, compiling and general project structure -> long message in thread

Francesco Pischedda19:11:53

Hi all! Recently I have been a bit confused by how defmulti/defmethod works and I could not find anything to help me to understand what was going on or how to better approach my problem. What I wanted to do was to provide a pluggable interface to store and retrieve some data in a pet project, something that could be changed via simple configuration, which could look like this:

(def config {:persistence {:backend :json-file
                           :base-path "/some/path"}})

or

(def config {:persistence {:backend :jdbc
                           :connection-string "sqlite:///:memory:"}})
The generic persistence interface, the implementation and the usage would be something like this:
;; persistence.clj
(ns persistence)

(defmulti store-data! (fn [config id data] (:backend config)))

(defmulti get-data (fn [config id] (:backend config)))

;; persistence.json_file.clj
(ns persistence.json-file
  (:require [persistence :refer [store-data! get-data]]))

(defmethod store-data! :json-file
  [config id data]
  (store-data-to-filesystem (:base-path config) id data))

(defmethod get-data :json-file
  [config id]
  (get-data-from-sowmhere (:base-path config) id))

;; client-code.clj
(ns client
  (:require [persistence :refer [store-data! get-data]]))

;; ...and some code dealing with data...
So far so good. When working with an active REPL I was evaluating all namespaces, everything was working as expected, the sun was shining and birds singing. Later I created an uberjar, deployed and the application started complaining that there was not an implementation of the defmulti for :json-file...life was not good anymore. It looks like that, because persistence.json-file was not required anywhere, the compiler did not know about it and that specialization was lost. The same happens if I start a fresh new REPL and evaluate the client namespace. I've "solved" it by requiring persistence.json-file in the client namespace but it feels wrong. Questions (finally): - Is there a way to say "please compile this namespace even if it is not referenced anywhere"? - Is there something wrong in the whole approach? i.e. is there a better way to build this configurable storage?

walterl20:11:48

FWIW, I do it the same way.

Francesco Pischedda20:11:51

Yes, it works, but somehow I feel it is breaking the abstraction

walterl20:11:10

Yeah, requiring an ns for no other reason than to get the methods defined feels odd, but it makes sense at least: how else is your Clojure to know that they exist? There are other, more dynamic ways to accomplish this (e.g. search for and loading code in the classpath), but they are a lot more involved.

Alex Miller (Clojure team)20:11:25

the code using the abstraction does not have to be the same code that's configuring the extensions by loading them

Alex Miller (Clojure team)20:11:41

but yes, someone has to load them

Francesco Pischedda21:11:21

Thanks! Can someone point me to some docs/articles to help me understand how to achieve the dynamic loading you are mentioning?

skylize22:11:44

I was just reading about this yesterday, but cannot remember where. Basically, if you do not require the namespace, then the JVM will not even bother building the class files. So you're doing it right by just requiring the namespace, even if it "feels weird".