Fork me on GitHub
Alex Miller (Clojure team)00:12:06

Sean, you did a great job explaining all that :)

💯 20
😊 4

I don't know if I will ever do anything with it, but made a quick copy of that conversation for future reference


I didn't really understand datafy/`nav` and the underlying protocols until I built the java.jdbc stuff Friday lunch time and played with REBL, to navigate through data lazily loaded from my test database... 🙂

👍 4
Noah Bogart01:12:56

slightly related to a question i had a couple nights ago: SET UP: I have a card game like magic where each card does something different. Currently, the cards are implemented as entries in a bunch of hashmaps, name->implementation-map. (The cards are too complicated to be simple edn.) I have the implementations spread over 11 individually namespaced files, based on the card type, for the 1420 cards currently in the game (~16k lines of clojure). I merge the 11 map into a single "card-defs" map and then get into it with the card name when I need a given effect. MY QUESTIONS: On the namespace/file side, is this the best way to do it? Should I have them as defs or functions that return the implementation map? Should I split the files into individual card files? How should I Is there some super obvious method for doing this that I've completely missed?


anyone doing AoC?


yes, check out the #adventofcode channel


@nbtheduke I would not recommend putting every card in its own separate file, really. I can't think of any benefit to doing so. If you are using an IDE that lets you find a card definition from its name in some relatively straightforward way, for when you need to do that, then it shouldn't really matter whether it is 1 file or 1420, but I am guessing that 1420 could easily become unwieldy in other ways.


There are maximum limits on the amount of code in one Java method, which turns into limitations on the maximum amount of code in one Clojure function, so my guess is that you would be semi-likely to hit that limit if you tried to put them all in one function.


I have heard that there are limits to the amount of code that can be in the "init" method of a single namespace, and if so, you could potentially hit those limits if you tried to put all of that in a single Clojure namespace.

Noah Bogart01:12:57

Yeah, I def don't want or need them in a single namespace or a single file, that sounds like hell

Noah Bogart01:12:51

Back when the card pool was much smaller all of the cards were in a single file, but when we hit the limit you mentioned we split it up


@nbtheduke On whether you should have vars or functions ... I wonder if macros would be interesting here with that much code to maintain. So you can alway change your mind later if you want to generate vars vs functions...

Noah Bogart02:12:59

Another card game I follow that's written in javascript has all of their cards in separate files, following a more OOP style. I suspect the way we do it is more clojure-y?

Noah Bogart02:12:09

Here's an example card from one of the files:

   {:flags {:rd-reveal (req true)}
    :access {:async true
             :effect (effect (show-wait-prompt :runner "Corp to use Explode-a-palooza")
                               {:optional {:prompt "Gain 5 [Credits] with Explode-a-palooza ability?"
                                           :yes-ability {:msg "gain 5 [Credits]"
                                                         :effect (effect (gain-credits :corp 5)
                                                                         (clear-wait-prompt :runner))}
                                           :no-ability {:effect (effect (clear-wait-prompt :runner))}}}
   card nil))}}


Also, with that many variations I would look at possible abstractions to find a data-oriented DSL to describe a card.

Noah Bogart02:12:48

Have any suggestions for data-oriented DSLs?


@nbtheduke Looks already pretty good. But yes, I understand that maintaining that many cards w/ text files could become painful/error-prone. Have you thought about storing those in a database?


By data-oriented DSL I meant designing a data model to be able to represent each card as clojure values. But I know nothing of your domain so I can't help much.

Noah Bogart02:12:08

@me1740 Is it possible to save functions in a database?


You will have to be able to represent the cards as data first.


But back to your original point. I don't see any problem with having 11 namespaces containing a bunch of vars with these maps for each type of card.

Noah Bogart02:12:18

Thanks! I appreciate the help


hey guys, a thought/question: say I would like to create a sequence (and retain its head) which, in order to compute it, I need to perform a side effect (say a network call). Also say, this sequence is quite short and I want to make sure that the computation happens now. Also say, that the computation has quite amount of business logic involved. so my options are: a. (doall (for: for is great for seq comprehension, but the doall and having all those logic (with side effects) inside the for macro feels kind of clunky to me. b. doseq: great for eager side effects, but bad for creating seqs. c. reduce or mapv are eager and will create a sequence, but will require complicating the computation function even more (because now we need to create a seq and also we do not have the for magic little helpers :let :when :while) d. loop-recur: well, this is the most “low level” and will do the work, but a the messier and least elegant. so what would you choose? add some missing options if i skipped them.


In your use case what do you think is the best way to separate the side effects from the pure transformations on the data?


Like for example, producing a sequence using your needed side effects (network calls), then using a transducer to forcefully apply pure transformations on the sequence to fit your business logic would in my opinion be much better than putting the side effects and business logic together in a large loop, doseq, etc.


How do I disable Aviso pretty exceptions? Something loads it, and it elides Java methods in its output, but I am trying to debug a Java problem...


does (clojure.repl/pst) give you the full stack trace, or is it formatted as well?


Thanks for the hint. I figured out that I can just not depend on Aviso Pretty in Leiningen's project.clj and whatever tries to prettify the exceptions will refrain from doing so, outputting a line that it does this only when I have it explicitly in my project's dependencies.


Different typic: Why do I need the first in the following code in order to get the actual element at tag? I would assume xml1-> should already fetch me the first element? (xml/emit-str (first (zip-xml/xml1-> zip tag)))

Alex Miller (Clojure team)13:12:42

I think it’s a coll bc there could be multiple tags (like <p> in a <div>)


Funny enough first gives me the tag element, including the inner <p> elements (multiple).


How do I stop from loading my tests? The tests folder is not included in Leiningen :source-paths, AFAIK.


They might get loaded through some default profile! > The refresh function will scan all the directories on the classpath for Clojure source files, read their ns declarations, build a graph of their dependencies, and load them in dependency order. (You can change the directories it scans with set-refresh-dirs.) (emphasis mine)


Thanks a lot! That worked!


For whatever reason it will start Midje and then everything goes downhill, because the tests modify the environment, e.g. by loading data from different paths than my development setup.


I'm writing something that needs to queue up events then execute an action on them asynchronoously, and wondering what the best way to do this is.

;; I'm polling for an event from point A
;; processing each event
;; Then I want to add the events to a queue, and perform an action on the collected events at certain time intervals. 

;; Would there be a problem with using go-loop for something like this? My current solution is something like: 
(let [c (chan 1000)]
  (go-loop []
    (<! (timeout 1000))
    (loop [events []]
      (if-let [event (poll! c)]
        (recur (conj events event))
        (do-something-to-events events)))
but I think I've heard that there are downsides to running processes like this with go blocks. Does anyone have suggestions for the best way to something like this?


would it be better to (thread (loop ... )) instead of go-loop?

Alex Miller (Clojure team)16:12:13

does do-something-to-events block?


currently yes, but could make it asynchronous


in my use case it is writing "events" to a database.


I think that my current implementation is deadlocking - because I get some of the writes to occur, but eventually they stop and I have to restart my app.


It looks like you're going to loop call do-something-to-events with an empty vector if the channel is empty.

Alex Miller (Clojure team)16:12:35

if you’re doing potentially blockable things then I would put it in a thread, not a go


could you elaborate on that? are go's unsuitable for stuff that may block? (io/network etc)


If you're going to block a while, you're going to starve the go thread pool.


how big is that?

Alex Miller (Clojure team)16:12:38

8 by default (but configurable)


i see. that's really not that hard to exhaust


I'll try it with a thread. Thanks @alexmiller @me1740 yeah, my example is a very simplified version of my actual implementation - forgot to include the check for if events is not empty


yes, that's why I usually say the go thread pool size is whatever concurrency you think you need minus one.


i use aleph for http stuff, how does that affect the "thread hogging" issue?


@schmee this looks like exactly what I needed to read. Thanks!


that's probably more low level than i'm willing to go


i haven't looked at the aleph default - if it's anything like the manifold default it uses an expanding threadpool and you don't generally have to worry about it. ymmv, but our stuff is largely non-blocking with some very occasional blocking ops and we've never had any trouble with either the aleph or manifold default threadpool configs


are you using deferred chaining or are you @'ing them?


only chaining, never @'ing - the one exception being unit tests which get a pass on @'ing


i see. i should do that too i guess


we used to have some usage of @ - but it only ended up causing trouble when we wanted to re-use and compose that code, so we excised it


Hey guys, I'm trying to write a procedure that adds keyvals based on some conditions and have a difficulty with an anonymous function inside -> macro

(def add-prec #(if (= (:channel %1) "book") (assoc %1 :prec "P0") %1)) ; also works with defn and (fn [] ..)

(defn subscribe-message [symbol channel]
  (-> {:event "subscribe" :pair symbol :channel channel}
this works perfectly, but I'd like to replace add-prec with an anonymous function
(defn subscribe-message [symbol channel]
  (-> {:event "subscribe" :pair symbol :channel channel}
      #(if (= (:channel %1) "book") (assoc %1 :prec "P0") %1)))
And I'm getting CompilerException java.lang.ClassCastException: clojure.lang.PersistentArrayMap cannot be cast to clojure.lang.ISeq, compiling:(core.clj:47:3)


I'm not sure where I'm trying to perform the implicit cast to seq


@kyselyradek That -> isn't doing what you think...


Oh, I'm not calling it, right?


Just realized


It's a purely syntactic transform and inserts the hash map literal into the first "slot" in that #( ... ) form


I'd use let for this function as written.


I just realised what the problem was. #() is just a definition, however -> macro puts the argument into a call


When I wrapped that into another set of parens, it works, which makes sense


(defn subscribe-message [symbol channel]
  (let [data {:event "subscribe" :pair symbol :channel channel}]
    (if (= (:channel data) "book") (assoc data :prec "P0") data)))


The thing is I'm going to have multiple subsequent assocs based on their conditions, that's why I started with -> in the first place


Ah, OK, so probably as-> is what you want here.


(defn subscribe-message [symbol channel]
  (-> {:event "subscribe" :pair symbol :channel channel}
      (as-> data (if (= (:channel data) "book") (assoc data :prec "P0") data))))


maybe cond-> would help?


In this case, yes, cond-> would be great there.


(let [data {:event "subscribe" :pair symbol :channel channel}]
  (cond-> data
    (= "book" (:channel data)) (assoc :prec "P0")))


(defn subscribe-message [symbol channel]
  (-> {:event "subscribe" :pair symbol :channel channel}
      (cond-> (= (:channel data) "book") (assoc data :prec "P0"))))


I'm going to look at those, but just out of curiosity, why are those superior to

(defn subscribe-message [symbol channel]
  (-> {:event "subscribe" :pair symbol :channel channel}
      (#(if (= (:channel %1) "book") (assoc %1 :prec "P0") %1))))
Is it just the readability?


(so you can start with -> and use cond-> or as-> as needed in subsequent branches)


Yeah, that (#(...) ) is horrible IMO 😞


You can tidy it up a little bit if you use the lib thread-first-thread-last-backwards-question-mark-as-arrow-cond-arrow-bang (sorry 😁)

🙊 16
👏 8

i'm with enforser here ... it looks like you are reinventing let there radek


I didn't even know about cond-> but it does feel like the best solution


Thank you both guys 🙂


Btw am I correct that the let binding is there just to have something to reference for conditionals?


That means, if my conditionals would actually check only with the function arguments, I could omit the binding and start directly with (cond-> {:event ....} ...?


Okay, I tried it myself. Yup, it works 😄


I meant this

(defn subscribe-message [symbol channel]
  (cond-> {:event "subscribe" :pair symbol :channel channel}
    (= channel "book") (assoc :prec "P0")))


Does it look like a proper Clojure code or should I prefer the let binding (enforser example) even though I will only need that reference on the macro's first line?


That's a matter of personal preference. Both are fine.

Noah Bogart17:12:30

if it's a single line I find it hard to read


It depends on the context. If you want to create the hash map before it's passed to the function, you can achieve the same effect with destructring:

(defn subscribe-message [{:keys [symbol channel] :as data}]
  (cond-> data
    (= channel "book") (assoc :prec "P0")))


but yes, the let binding was just to avoid having a literal map definition repeated multiple times


Okay, thank you! 🙂


Okay, another one 😄 If I have let's say 3 transforms per condition, should I prefer repeating the condition

(defn subscribe-message [symbol channel]
  (cond-> {:event "subscribe" :pair symbol :channel channel}
    (= channel "book") (assoc :prec "P0")
    (= channel "book") (assoc :freq "F0")
    (= channel "book") (assoc :length "100")
    (other-cond) (other-assoc)))
Or nesting with -> macro?
(defn subscribe-message [symbol channel]
  (cond-> {:event "subscribe" :pair symbol :channel channel}
    (= channel "book") (-> (assoc :prec "P0")
                           (assoc :freq "F0")
                           (assoc :length "100"))
    (other-cond) (other-assoc)))


Just use one assoc with multiple keys


(assoc :prec "P0" :freq "F0" :length "100")


(assoc :prec "P0" :freq "F0" :length "100")


OMG I'm an idiot 😂 😂 😂


Some folks prefer merge for that: (merge {:prec "P0" :freq "F0" :length "100"})

👍 4

I guess I should read docs more often


but in general, if you want to perform two very different operations based off the same condition then I just generally follow my heart on threading in the cond-> value, or using the same condition. However, if I'm duplicating the condition, then I throw it in a let so it's only evaluated once.

(cond-> {}
  true (-> do-one-thing do-another-thing))
(let [is-true? true]
  (cond-> {}
    is-true? do-one-thing
    is-true? do-another-thing))

👍 4

Yeah, that makes a lot of sense. Another question—what's the best way to learn these best practices?


they probably just come with time, practice, and paying attention to what other people are doing. I've also found reading the examples on both clojuredocs and to be helpful. Every once in a while I'll just look at what exists in clojure.core that I don't know about and try to understand what it's for / could be used for.


can also read through the clojure documentation, which has sections on threading, destructuring, etc.


not sure there's ultimate best practice. if your co-workers starts to cry whilst code review then you have been bad 🙂 keep it simple. simple is good.


I was lucky, because my first software job was/is in clojure(script), so I had plenty of examples and experienced clojure devs around


@kyselyradek has a ton of awesome information in it - great to look at examples of specific things from the core library and how they can be used.

👆 4

Thank you guys, really! 🙂 🙂


Yeah, I guess I'll have to catch the language-specific practices on the go 🙂


It definitely takes a while -- clojure.core is a pretty huge surface to learn! I've been doing Clojure in production for nearly eight years and I often still trip over a new-to-me core function that makes my code simpler.

Alex Miller (Clojure team)18:12:15

great categorized reference to core


re: lesser known core fns: some of us just rediscovered the usefulness of max-key in solving #adventofcode problems


question ... how does one get the diff between two java.time.Instant instances? I'd have thought the following would work but I get "No matching method" error. (.between java.time.Duration inst1 inst2)


duration is a static method: (Duration/between inst1 inst2)


is there a vim plugin that just connects to the built-in socket repl like emacs inf-clojure?


or you could try neoterm and similar ilk


@lockdown- you could also try #vim


is it useful to think of dataify/nav as something like a zipper over external data?


it is like part of a zipper


zippers have the ability to rebuild back to the root, and to replace nodes at a given place with other nodes


dataify/nav is only like the traversing part of a zipper


@hiredman @shaun-mahood, thanks, some reading to do


@ghadi thanks - haven't really done much interop thus far so got a little confused there.