Fork me on GitHub
#clojure
<
2018-12-04
>
alexmiller00:12:06

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

💯 5
😊 1
andy.fingerhut00:12:33

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

seancorfield00:12:16

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... 🙂

👍 1
NoahTheDuke01: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?

Average-user01:12:23

anyone doing AoC?

thegeez10:12:09

yes, check out the #adventofcode channel

andy.fingerhut01:12:48

@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.

andy.fingerhut01:12:30

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.

andy.fingerhut01:12:18

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.

NoahTheDuke01:12:57

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

NoahTheDuke01: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

benoit02:12:51

@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...

NoahTheDuke02: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?

NoahTheDuke02:12:09

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

"Explode-a-palooza"
   {:flags {:rd-reveal (req true)}
    :access {:async true
             :effect (effect (show-wait-prompt :runner "Corp to use Explode-a-palooza")
                             (continue-ability
                               {: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))}}

benoit02:12:23

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

NoahTheDuke02:12:48

Have any suggestions for data-oriented DSLs?

benoit02:12:57

@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?

benoit02:12:02

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.

NoahTheDuke02:12:08

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

benoit02:12:08

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

benoit02:12:08

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.

NoahTheDuke02:12:18

Thanks! I appreciate the help

ido05:12:00

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.

chrisulloa05:12:54

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

chrisulloa05:12:10

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.

urzds11:12:53

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...

sashton13:12:49

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

urzds13:12:12

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.

urzds12:12:10

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)))

alexmiller13:12:42

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

urzds14:12:08

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

urzds14:12:24

How do I stop clojure.tools.namespace.repl/refresh from loading my tests? The tests folder is not included in Leiningen :source-paths, AFAIK.

aisamu14:12:35

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)

urzds14:12:41

Thanks a lot! That worked!

urzds14:12:08

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.

enforser15:12:48

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)))
    (recur))
  c)
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?

enforser15:12:52

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

alexmiller16:12:13

does do-something-to-events block?

enforser16:12:33

currently yes, but could make it asynchronous

enforser16:12:54

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

enforser16:12:52

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.

benoit16:12:55

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

alexmiller16:12:35

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

valerauko16:12:20

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

benoit16:12:03

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

valerauko16:12:44

how big is that?

alexmiller16:12:38

8 by default (but configurable)

valerauko16:12:31

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

enforser16:12:55

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

benoit16:12:07

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

valerauko16:12:19

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

enforser16:12:34

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

valerauko16:12:29

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

mccraigmccraig16:12:58

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

valerauko16:12:12

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

mccraigmccraig16:12:20

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

valerauko16:12:48

i see. i should do that too i guess

mccraigmccraig17:12:41

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

Radek17:12:01

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}
      (add-prec)))
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)

Radek17:12:20

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

seancorfield17:12:56

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

Radek17:12:07

Oh, I'm not calling it, right?

Radek17:12:12

Just realized

seancorfield17:12:32

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

seancorfield17:12:10

I'd use let for this function as written.

Radek17:12:34

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

Radek17:12:51

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

seancorfield17:12:58

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

Radek17:12:47

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

seancorfield17:12:03

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

seancorfield17:12:50

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

enforser17:12:13

maybe cond-> would help?

seancorfield17:12:39

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

enforser17:12:10

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

seancorfield17:12:14

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

Radek17:12:51

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?

seancorfield17:12:55

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

seancorfield17:12:07

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

danielneal17:12:08

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 https://github.com/randomcorp/thread-first-thread-last-backwards-question-mark-as-arrow-cond-arrow-bang (sorry 😁)

🙊 4
👏 2
kulminaator17:12:18

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

Radek17:12:50

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

Radek17:12:56

Thank you both guys 🙂

Radek17:12:35

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

Radek17:12:24

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

Radek17:12:12

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

Radek17:12:32

I meant this

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

Radek17:12:57

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?

didibus17:12:01

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

NoahTheDuke17:12:30

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

enforser17:12:53

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")))

enforser17:12:52

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

Radek18:12:46

Okay, thank you! 🙂

Radek18:12:59

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)))

seancorfield18:12:20

Just use one assoc with multiple keys

enforser18:12:37

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

seancorfield18:12:38

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

Radek18:12:03

OMG I'm an idiot 😂 😂 😂

seancorfield18:12:18

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

👍 1
Radek18:12:09

I guess I should read docs more often

enforser18:12:53

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.

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

👍 1
Radek18:12:28

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

enforser18:12:55

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 http://conj.io 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.

enforser18:12:06

can also read through the clojure documentation, which has sections on threading, destructuring, etc. https://clojure.org/guides/threading_macros

kulminaator18:12:38

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.

enforser18:12:14

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

shaun-mahood18:12:18

@kyselyradek https://www.manning.com/books/clojure-the-essential-reference 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.

👆 1
Radek18:12:42

Thank you guys, really! 🙂 🙂

Radek18:12:05

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

seancorfield18:12:14

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.

1
alexmiller18:12:15

great categorized reference to core

2
taylor19:12:10

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

markw20:12:37

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)

ghadi20:12:56

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

johnj20:12:57

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

dominicm20:12:43

or you could try neoterm https://github.com/justinmk/nvim-repl and similar ilk

enforser20:12:27

@lockdown- you could also try #vim

zugnush20:12:09

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

hiredman20:12:50

it is like part of a zipper

hiredman20:12:22

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

hiredman20:12:57

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

zugnush20:12:37

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

markw21:12:10

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