Fork me on GitHub
#beginners
<
2020-11-12
>
st3fan00:11:05

I had a day off which I used to re-learn Clojure. I’m converting some GitHub bots that I wrote in Go to Clojure and it has been pretty interesting so far.

🤓 6
🙂 3
dgb2301:11:39

Hi! is there a standard clojure function which does the equivalent of (spec/conform (spec/cat …) […])? This is so useful I assume there must be a similar thing outside of spec that people use.

seancorfield01:11:41

@denis.baudinot I would say that is the "standard clojure function" for it -- but it depends on exactly what spec functionality you're referring to there...

seancorfield02:11:14

If you mean "take a list of names and a list of values and make a hash map" then there's zipmap but it doesn't do any conformance on the actual values:

user=> (s/conform (s/cat :a int? :b string?) [42 "Hello"])
{:a 42, :b "Hello"}
user=> (zipmap [:a :b] [42 "Hello"])
{:a 42, :b "Hello"}
user=> (zipmap [:a :b] ["Forty Two" 13])
{:a "Forty Two", :b 13}
user=> 

👍 3
dgb2302:11:02

This is exactly what I meant, thank you for the clarification. (I like the spec version more when looking at this and thinking about it though. Conforming values in general seems to be a really useful thing to do.)

Mark Wardle09:11:04

Hi all. I have a weird failing test that may be the result of AOT compiling and only occurs on a test first run, after deleting the contents of the classes/ directory. I have a defrecord containing numbers, strings and a java.time.LocalDate. Equality testing works fine based on the value of the LocalDate in local testing (even though the objects do not have the same identity) AND works fine on 2nd and subsequent runs of a test that serializes then deserializes the record into a key-value store (mapdb). The problem is on the FIRST run of the test which somehow decides that two LocalDates with the same value (but different identities) are not equal. It feels as if it is something to do with a cache but it simply shows up my ignorance of the process of AOT and its interaction with running with JIT compiled code. EDIT: Actually fascinating as the problem only rears its head if I run tests by executing AOT on the same clj run as the test - and doesn’t happen if I manually run AOT first and THEN the test run.

noisesmith16:11:25

if you run (defrecord Foo []) twice, you get two definitions of Foo, and they have the same name and definition, but are not =

noisesmith16:11:56

creating instances of Foo, then aot compiling its ns, then creating Foo again, will create two totally different Foo

Mark Wardle17:11:37

Oh. Yikes. I need to AOT because the class is used by an external java library. But that definitely explains some screwy things I’ve been getting such as “x can’t be cast to x” where x=x after even reloading the REPL.

Mark Wardle17:11:32

Thank you for the explanation.

noisesmith17:11:32

if you need AOT, definitely do it as a separate step and be sure the results of it are loaded before creating instances

noisesmith17:11:12

and if you need to reload namespaces that use defrecord / deftype etc. you need to also destroy and recreate all those instances

noisesmith17:11:47

which is part of why stuartsierra/component and weavejester/integrant libraries are so useful - they automate this process

👍 3
Mark Wardle17:11:23

Ah! That makes sense thank you very much!

Eamonn Sullivan09:11:55

I have a very beginners, possibly beginner functional, question: I have a collection of a couple hundred things. I have to filter those things using a REST call (to get an answer to a boolean question that I'm filtering on). There's no bulk version of the REST call I need to make, but making a couple of hundred sequential HTTP requests in a row is taking too long (`(filter is-it-a-thing? coll)`). What tool would a more experienced Clojure person reach for in this situation?

Eamonn Sullivan10:11:37

Looking at clojure.core.reducers/fold and family.

Eamonn Sullivan12:11:26

I too stupid for fold, but result1 is a lot faster than result2.

user> (def result1 (remove nil? (pmap #(if (is-a-thing? %) % nil) things)))
#'user/result1
user> (def result2 (filter is-a-thing? things))
#'user/result2
user> (time (count result1))
"Elapsed time: 7988.541086 msecs"
73
user> (time (count result2))
"Elapsed time: 29946.487776 msecs"
73
user> 
I'm guessing this could be muchly simplified.

Eamonn Sullivan13:11:45

Or another example:

user> (time (def result1 (into [] (filter is-a-thing? things))))
"Elapsed time: 23648.239951 msecs"
#'user/result1
user> (time (def result2 (into [] (remove nil? (pmap #(if (is-a-thing? %) % nil) things)))))
"Elapsed time: 5181.302774 msecs"
#'user/result2
user> 

Old account10:11:37

Hello! are there any naming conventions for vars in Clojure beside earmuffs *var* . I did stumble upon something like this +sub-name+ and unsubscribe* is it something common or just up to the author?

borkdude10:11:37

I think +foo+ is not that common. foo* is, usually to indicate a variation or a more low level alternative

✔️ 3
bronsa11:11:56

+foo+ is often used in other lisps to indicate a constant value, but not really in clojure

Jim Newton13:11:27

need clever way to iterate through a sequence. I have sequence, input-sequence and a function, f, which I can call on each element of the sequence. The problem is that f is expensive to call, so I want to call it as few times as possible. I can suppose that f always returns one of true, false, or :dont-know I want to return false if the sequence is empty, without calling f I want to return true if the f returns true on any element of the sequence. I want to return false if f returns false on every element of the sequence. otherwise I want to return :dont-know If I create a sequence like this (map f input-sequence) this sequence will be lazy. Am I guaranteed that f will only be called when I access that element of the output sequence? I.e., could first check the output sequence to see if it contains true, and if not, then check to see if it only contains false

Daniel Stephens15:11:37

you can't perform this logic lazily so not sure the whole unchunking stuff matters. > I want to return true if the f returns true on any element of the sequence. Every element will have to be checked.

andy.fingerhut15:11:01

As soon as one element causes the supplied function f to return true, I would guess he wants to avoid calling f again.

Daniel Stephens15:11:27

Oh yeah, my bad, I see what you mean 👍

andy.fingerhut15:11:31

As soon as an element is reached that causes f to return true, there is no need to continue evaluating.

bronsa13:11:24

laziness in clojure doesn't guarantee 1 at a time laziness

Jim Newton13:11:46

Ahh, so I need to do my own interation then to be sure?

bronsa13:11:56

but you can (map f (unchunk sequence)) to turn it into 1 at a time

bronsa13:11:19

the usual definition of unchunk would be

(defn unchunk [s]
  (when (seq s)
    (lazy-seq
      (cons (first s)
            (unchunk (next s))))))

bronsa13:11:01

alternatively you could reduce over the collection with a boolean accumulator, and short circuit using reduced to return true

Jim Newton13:11:48

yes reduce/reduced or even loop/recur will do the trick of course, but the logic is contorted. My suspicion is that the code is more concise If I can just check membership. without resorting to inline iteration.

bronsa13:11:49

if I understand correctly, something like (reduce (fn [_ el] (case (f el) true (reduced true) false false :dont-know (reduced :dont-know)) false coll)

bronsa13:11:21

I personally find this type of operation more natural to express/understand in terms of folds like above

Jim Newton13:11:52

@bronsa, won't your function return :dont-know if it encounters a :dont-know? It should return true if there is a true anywhere in the list, even after a :dont-know.

bronsa13:11:25

ah, yes :)

bronsa13:11:16

(reduce (fn [b el] (case (f el) true (reduced true) false b :dont-know :dont-know) false coll) this then I think?

Jim Newton13:11:36

(let [values (map f (unchunked intput-sequence))]
  (cond (some true? values)
        true

        (every? false? values)
        false

        :else
        :dont-know))
how about this?

bronsa13:11:18

¯\(ツ)

bronsa13:11:43

it works I guess, tho it's more expensive than the single scan using reduce

bronsa13:11:14

FWIW forall would (every? false? values)

bronsa13:11:44

and member would be (some true? values)

Jim Newton13:11:19

good suggestions. I normally find forall more readable, but in this case (every? false? values) is more readable.

Jim Newton13:11:09

and them (some? true? values) or (some true? values) I always mix up some vs some?

bronsa13:11:26

some is the pair of every? :)

bronsa13:11:40

some? is non-nil

bronsa13:11:59

bit confusing but if you understand why they're named that way, it makes sense

bronsa13:11:33

i.e. some is not a predicate, it doesn't return true/false, but the first non falsey value

bronsa13:11:40

in other languages some is better named find-first or things like this

Jim Newton13:11:19

is some the same as (first (filter ...)) ?

bronsa13:11:09

more like (first (keep (if keep returned on truthy rather than non-nil)

bronsa13:11:10

let's say some is (first (filter identity (map f :)

Jim Newton13:11:10

back to the previous question. what's more readable, the use of map depending on the lazy nature, or an explicit but very clever reduce which computes the value without dependence only laziness ?

bronsa13:11:56

I personally find reduce significantly more readable here, and not particularly clever/obfuscated

bronsa13:11:36

I'll give you that your version is more "declarative"

bronsa13:11:06

but this sort of folds is super natural in FP so after a bit of getting used to it, it becomes second nature

bronsa13:11:41

but really, it's up to personal preferences if you're not too fussed about the extra linear scans (which you probably shouldn't be)

bronsa14:11:34

the one thing I'd say tho, is I'd advise you to prefer functions available in the clojure.core stdlib vs non standard macros that achieve the same thing (like that forall thing)

bronsa14:11:51

makes it easier for other people to understand your code

Jim Newton14:11:34

Don't get me wrong. I love reduce, and I use it a lot. However, in this case (maybe I'm wrong, thus asking) am I trying to be too clever. For example, you're first implementation was wrong as well. Is there another bug in there somewhere. For example, does it really do the correct thing on the empty sequence. I suppose in either case I need to write unit tests to make sure, so perhaps corner cases should be considered moot.

bronsa14:11:25

well, my first implementation was wrong cause I didn't read the spec well enough :) and re: empty sequence, yes, the behavior of reduce is defined to return the init value if the sequence is empty (from the docs:

If val is supplied, returns the result of applying f to val and the first item in coll, then applying f to that result and the 2nd item, etc. If coll contains no items, returns val and f is not called.

bronsa14:11:11

but again, readability is subjective. use whatever feels easier to you, I still find your version very readable

Jim Newton14:11:19

actually I was under the impression that reduce calls the function with 0 arguments if the sequence is empty.

bronsa14:11:27

if the init val is not supplied

Jim Newton14:11:38

ahhhhhhhh...

bronsa14:11:02

protip: forget about the non-init arity of reduce

☝️ 9
Jim Newton14:11:30

in that case I have several pieces of code I can clean up. Because I sometimes pass a mult-arg version of fn to reduce just for the empty-sequence case.

Jim Newton14:11:08

that's something that always annoyed me about clojure's reduce. Now perhaps I don't need to be annoyed.

Jim Newton14:11:52

it's not clear from the doc, whether if I supply a singleton list, AND an init value, whether the function is called. (reduce (fn [acc item] (+ acc (count item))) 0 '("a" "ab"))

bronsa14:11:06

If val is supplied, returns the result of applying f to val and the first item in coll

bronsa14:11:34

doesnt' matter how many elements are in coll (unless coll is not empty)

bronsa14:11:05

hang on, in that example '("a" "ab") is not a singleton list

bronsa14:11:07

it has two elements

Jim Newton14:11:17

yes exactly. reduce (as it seems to by experimentation) is supposed to sum the counts. which it does, even if the sequence is a singleton '("a")

Jim Newton14:11:45

It does the right thing, but the docstring could be clearer in my opinion.

bronsa14:11:07

clojure docstrings are notoriously terse

bronsa14:11:56

but I find this behavior clear from the docstring tbh

Jim Newton14:11:13

that fact that in some cases clojure's reduce calls the function with 0 arguments to get the unit value is bizare and clever. I don't know of other functional languages that do this. well I only know about Common Lisp and Scala.

andy.fingerhut14:11:07

I had heard that Clojure's behavior of reduce was influenced by Common Lisp's behavior with a similar function.

Jim Newton15:11:06

the Common Lisp reduce function is a lot of functions rolled into one.

Jim Newton15:11:29

If I recall correctly, you can specify right to left traversal, or that only a sub-range be traversed, and lots of stuff. in my opinion it's too much to think about.

andy.fingerhut15:11:57

I mention it because its documentation explicitly mentions that the function you provide to it can be called with 0 or 2 arguments.

Jim Newton17:11:49

hmm.. i forgot about that part. But now I seem to remember that it is indeed not called with 0 arguments if you provide an :initial-value.

andy.fingerhut17:11:36

The description of the behavior of when the function f is called with 0 args is pretty close between the Common Lisp Hyperspec and Clojure's reduce functoin.

dpsutton14:11:15

Cursive and cider both have clojuredocs available and built in for more verbose info with examples

bronsa14:11:58

yes, the no-init behavior of reduce is unfortunate and I'm pretty sure if the clojure/core team were to design clojure from scratch, it would not be in the language

bronsa14:11:15

it's an unfortunate wart of clojure being backwards compatibile (which is an asset most of the time)

Jim Newton14:11:29

it is nice for functions like + and *

Jim Newton14:11:40

anyway, I'm happy to realize that I don't need to include the 0-ary version of the function every time when I'm not certain about the length of the input sequence.

Jim Newton14:11:48

thanks for that revelation

Jim Newton14:11:04

QUESTION about the distinct function. The documentation does not define anything about the order of the elements in the returned sequence. Or even whether it always return an = sequence if there are no duplicate elements. Can I depend on the order returned. Or do I need to write my own version of distinct if I care about the order?

Jim Newton14:11:52

A few years ago I worked for a company which maintained a proprietary lisp implementation. At some point we improved the hashing function of the standard hash table to make it faster, or more dispersive. Enough of our customers complained that their code was breaking. Some of their code was depending on the iteration order of the map-over-hash function that we had to revert the changing, leaving in place an inferior function. In retrospect, the original hash iterator should have been intentionally non-deterministic.. But alas....

Alex Miller (Clojure team)14:11:59

distinct retains order of the original sequence

Jim Newton14:11:52

thanks @alexmiller is that a feature that's guaranteed ? For example it would be faster (at least on large sequences), if it added the elements to a set, and then extracted them from the set, thus not maintaining the order. Also maintaining the order is not really a specification. For example there are two ways to uniquify (1 2 3 2) maintaining order. either (1 3 2) or (1 2 3) . one maintains left-to-right order and the other maintains right-to-left order.

Alex Miller (Clojure team)14:11:33

I would consider it to be implied by the current docstring and likely to break people if it changed

Alex Miller (Clojure team)14:11:47

so I can't imagine it would change

Alex Miller (Clojure team)14:11:13

if you want a different behavior, you'll need your own impl

Alex Miller (Clojure team)14:11:44

current impl always keeps the first seen element

Jim Newton14:11:34

Thanks for the info.

Returns a lazy sequence of the elements of coll with duplicates removed.
Returns a stateful transducer when no collection is provided.
is this the docstring you're referring to?

Jim Newton15:11:37

SBCL common lisp tries to maintain order, and is fast for short lists. Allegro Common Lisp makes no promises about order, and is faster for large lists. Allegro maintainers are more interested in Big Data anyway. So I'm not sure which one is really better. There are lots of small lists in the world.

arielalexi17:11:20

Hey, I want to add a new namespace to my project. I am using leiningen with Intelij. I checked the libary with https://clojars.org/java-http-clj , than added to the :dependencies in the project.clj and it also presented in the Leiningen dependencies tree. But when I am adding it to my core.clj :requires and reload the core file to the REPL I am getting an error of FileNotFound. Is there other place I need to add?

dpsutton17:11:17

what are you adding to the require part of your namespace?

dpsutton17:11:52

and want to point you to the documentation of the library: https://github.com/schmee/java-http-clj#examples

arielalexi17:11:08

I did use the documentaion of the libray, and yet getting an error only when adding

[java-http-clj.core :as http]
this is the error :
CompilerException java.io.FileNotFoundException: Could not locate clojure/spec/alpha__init.class or clojure/spec/alpha.clj on classpath., compiling:(java_http_clj/core.clj:1:1) 

andy.fingerhut17:11:42

That namespace clojure.spec.alpha requires Clojure 1.9 or later. Are you using Clojure 1.8 or earlier in your project?

andy.fingerhut17:11:09

I get that error message you show as well if I try to require the java-http-clj.core namespace from inside of a project that uses Clojure 1.8.0

arielalexi17:11:30

@U0CMVHBL2 update in your project.clj to be [org.clojure/clojure "1.10.0"] than you will not have that error

andy.fingerhut18:11:50

I understand. I was trying to convey that information to you by reproducing the error you are seeing 🙂

andy.fingerhut18:11:39

If you go to Clojure 1.10.0, I'd recommend going to 1.10.1, since there are a few fixes there relative to 1.10.0, especially if you executed code in your projects from a user.clj init file.

3
acim117:11:42

Can someone confirm if I can invoke (use-fixtures...) multiple times (profitably) in a test? Basically, I want to register two fixtures like

(use-fixtures :once (fn [f] (start-db) (f) (stop-db))  ; wrap the whole suite, I want to get one connection for the test
(use-fixtures :each (fn [f] (f) (clean-up-test-data))) ; after each test, make sure created data is cleaned up

Darin Douglass17:11:53

Best way to find out is to just try it! (Spoilers: yes you can do that)

acim117:11:51

Sure, I understand, but I didn't want to go down a road where I start questioning the concept because PEBCAK. Now that I know I'm on the right path, I can proceed with confidence! Thank you

Daniel Stephens17:11:31

it doesn't work for the same 'type' of fixture

(use-fixtures :each (fn [f] (println "not here") (f)))
(use-fixtures :each (fn [f] (println "here") (f)))

(deftest a
  (is (= 1 1)))
will only print "here", but you can supply multiple fns as args in one call:
(use-fixtures :each
              (fn [f] (println "here") (f))
              (fn [f] (println "and here") (f)))

👍 3
Joe21:11:15

What's a relatively painless way to do a PAAS hosting of a docker container running a JAR built from a Clojure project? Not looking for scale or configurability, just something simple that I can hit with HTTP requests. I looked at Heroku, but looks like a lot to learn and before I start I want to make sure I'm not going down a blind alley, or see if there's a better alternative.

phronmophobic21:11:29

It depends on what you're looking for. I've been using $5 digital ocean servers as well as the equivalent amazon instances and just run the jar directly

phronmophobic21:11:43

amazon also has options like like lightsail and elastic beanstalk

Joe21:11:45

Thanks, it sounds like you and @U0MDMDYR3 just skip the docker step. Maybe I should try without for now. I have very little idea on what the options or best-practices are.

deactivateduser21:11:43

Yeah I’m not going to claim that what I’m doing is best practice, but it works well for me at this tiny “hobby” scale.

deactivateduser21:11:31

I basically don’t think about Heroku much at all, now that it’s setup - it’s all just source control (GitHub) shenanigans, some of which cause a new deployment (i.e. commits to my main branch).

dgb2321:11:12

I don’t think docker would be necessary for anything that is small to medium sized and/or doesn’t have very frequent updates for java deployments right?

dgb2322:11:43

also is my assumption correct that the JVM already abstracts from the platform? You can bundle it so it includes all the dependencies in a single jar is what I know. I did my last java production deployment 10ish years ago so idk.

deactivateduser22:11:00

Yeah for my Heroku I build and run an uberjar. I tried running from source, but for some reason that more than doubled the runtime memory requirements, which blew through the (small, strict) amount of memory Heroku gives you (512MB for a hobby-tier dyno).

dgb2322:11:01

last time I checked out Heroku, which is a while ago, I got turned off by the fact that you can’t write to files. Most small web applications have some sort of file storage requirement like a SQLite db, some primitive logging or something like that.

deactivateduser23:11:17

Yeah could be. My chat bot has (so far) remained mercifully free of any persistent state that it can’t resurrect from 3rd parties, which has greatly simplified the runtime environment.

Joe01:11:54

Hm, that might be a disqualification for Heroku. Though I see the do offer separate DBs, including Kafka

practicalli-johnny08:11:15

I use Heroku without docker, it automatically builds from source using the usual Clojure build tool. No need to add any extra layers. You can even get CircleCI to deploy to Heroku, using the Heroku orb on CirclCI. It's also easy to add a pipeline in Heroku if you want a staging environment, then simply promote the staging image when ready. If you really need persistent file system, use S3 or one of the plugins. Or use persistent storage/database suitable for the cloud.

cdpjenkins09:11:47

I use Heroku (to which I publish a jar file, not source) for a tiny app that I haven’t updated in years. It was pretty painless to set up and still works after a few dyno upgrades. For me, dockerising my toy app would have been additional complexity without benefit.

3
deactivateduser21:11:25

I have a little open source Clojure chat bot running on Heroku, if a worked example is helpful: https://github.com/pmonks/futbot It runs on the “hobby” tier ($7/mth), since I didn’t want it getting quiesced when “inactive” (which it wasn’t, but I was concerned that Heroku wouldn’t detect any activity since the chat bot doesn’t have any traditional web traffic). That said, I don’t use Heroku’s Docker capabilities for this - I just deploy an old-skool “dyno” (which is built by Heroku).

thanks 3
deactivateduser21:11:50

Is there an established naming convention for “pure” namespaces (i.e. those that contain nothing but pure functions) vs “impure” namespaces (that contains state, for example)? If not, any suggestions for such a thing? e.g. a ! suffix for impure namespaces - my.impure.ns! The background is that I (try to) make as many of my namespaces as possible “pure” in this way, with state handling centralised into a few stateful namespaces, and there are times where it would be nice to know which is which, simply by looking at namespace names (e.g. filenames), and also figure out whether I’ve accidentally polluted one of my pure namespaces...

phronmophobic21:11:36

what makes a namespace impure?

deactivateduser21:11:48

e.g. (def state (atom {}))

deactivateduser21:11:08

With code in that ns swap! ing the value of the atom.

deactivateduser21:11:48

Basically any ns that contains any fns that don’t rely solely on their arguments to do their thing.

phronmophobic21:11:06

using swap! at the top level or just using swap! somewhere inside one of the functions?

phronmophobic21:11:39

what if the swap! is using an argument passed to the function and not a global atom?

deactivateduser21:11:39

And to be clear, it’s not limited to atoms.

deactivateduser21:11:51

A function that declares an atom locally (e.g. in a let) could still be pure, no?

deactivateduser22:11:03

I do this for interop with nasty mutable Java code sometimes, for example, but the fn itself is still pure.

phronmophobic22:11:17

what's the filename look like for a filename namespace with a ! in it?

deactivateduser22:11:16

Haven’t gotten that far yet, though (in-ns 'my.impure.namespace!) appears to work just fine. I’m open to other suggested conventions, of course - indeed that was my original question! 😉

phronmophobic22:11:46

in-ns is extremely rare in my experience. usually ns is used at the top of namespaces and require is used to reference code in other namespaces

deactivateduser22:11:39

I understand that - I’ve rarely if ever used it before. I was simply using that at the REPL to quickly check it. No point creating a foo!.clj file if (in-ns 'foo!) doesn’t even work…

deactivateduser22:11:06

Again, let’s not get hung up on the specific convention of using a ! suffix on the namespace - I’m more interested if anyone has used any convention for indicating pure vs impure namespaces, and if so what that might be.

phronmophobic22:11:50

yea, I'm not sure how that would interact with :gen-class

phronmophobic22:11:22

I'm also not sure pure/impure namespace is a clear distinction

phronmophobic22:11:48

and there's some kinds of impurity that are "acceptable" (ie. interning a var via def)

phronmophobic22:11:07

memoization is another type of impurity that is often "acceptable"

deactivateduser22:11:29

Right - concrete vs abstract side effects, to paraphrase Bertrand Meyer.

deactivateduser22:11:56

Let me ask you though: do you think the notion of a pure fn is, or can be, well-defined?

deactivateduser22:11:01

Because if so, then all I’m referring to by “pure namespace” is a namespace that contains nothing but pure fns (and I’d love to hear of a better label, if such a thing exists!).

phronmophobic22:11:24

I think it can be defined well enough. what kind of impure namespaces do you want? the only kind I can think of is if it's necessary for clojurescript interop

phronmophobic22:11:27

otherwise, I generally expect all namespaces to be pure

phronmophobic22:11:47

or at least, idempotent when loaded

phronmophobic22:11:30

another example is when you have a global registry, which can be considered "acceptably" impure in some cases

deactivateduser22:11:14

So here’s one concrete example - not the only one, probably not even a good one - but it’s the one I’m most familiar with. 😉 I have a small chat bot (I happened to link it just above, fwiw) that runs as a standalone command line app. The bootstrap code for that app reads a config file and calls some REST APIs, and then caches that data away (in memory), and starts a bunch of timed jobs (functions that run at specific times throughout the day). Those “job functions”, and all of the underlying functions that they call, are pure - they receive all of the data they need to do their thing as arguments, and they don’t read or persist any state anywhere else. The “bootstrap” code however, is far from pure - it reads all sorts of things from the greater environment (config files, internet services), and then squirrels it away for later. Now I’ve tried hard to keep that “bootstrap” code as small and isolated from the rest of the (pure) codebase as possible, but at times I’ve let some statefulness accidentally creep in, or opened the wrong file because I’ve been away from the code for some time, or whatever. That’s where a naming convention would be helpful.

phronmophobic22:11:12

I would move all the bootstrap code into functions and only call that code from a main function. there are also libraries like https://github.com/stuartsierra/component that can help

phronmophobic22:11:30

then your namespaces are pure again

deactivateduser22:11:18

Yeah I use mount , which is similar in intent to component, but lighter weight.

deactivateduser22:11:55

But I basically consider the state that mount creates as being equivalent to an atom, and so don’t let those (global) vars creep into my pure functions anywhere.

deactivateduser22:11:18

Instead I pass everything around, and keep references to the mount-managed state to the “impure” namespaces only.

deactivateduser22:11:47

For one thing it makes testing substantially easier (and yes mount supports testing, but it’s even easier to test without having it there at all).

deactivateduser22:11:44

Oh and the impure bootstrap logic is (mostly) within my -main fn (or at least the same main namespace).

deactivateduser22:11:12

But still, I’d like to be able to more quickly / easily differentiate the (impure) main ns from my pure nses.

phronmophobic22:11:01

a function that deals with state doesn't infect a namespace

phronmophobic22:11:49

as long as loading a namespace doesn't have any unacceptably impure side effects, then I would consider the namespace itself pure for all practical purposes. no?

deactivateduser22:11:49

Perhaps “pure” is a misleading label then.

deactivateduser22:11:14

The differentiating criteria is “can I call any fn in this namespace, without having to setup state anywhere else first”.

deactivateduser22:11:28

When using component / mount and similar libraries, fns that directly consume the state created by those libraries no longer meets that criteria. i.e. you have to have those libraries create some form of state first (even if it’s just hardcoded “test data” type state).

phronmophobic22:11:40

ah ok. I was thinking of something different. I have marked functions with side effects using the ! suffix

deactivateduser22:11:12

Right, which is why my first naive thought was to use that same suffix at the namespace level. 😉

deactivateduser22:11:27

Obviously I’d much rather adopt an existing convention, if such a thing exists!

phronmophobic22:11:15

yea, I'm just not sure if namespace, rather than function by function, is the right granularity

phronmophobic22:11:29

like if a namespace contains both, it's still fine using a pure function from that namespace

phronmophobic22:11:15

additionally, if in a new version, you want to add a function that is impure to a namespace, what do you do? do you create a new namespace to avoid changing the namespace name? that doesn't seem worth it if it otherwise logically still belongs

deactivateduser22:11:51

My speculative thought would be that that would indicate a design problem.

phronmophobic22:11:01

I'm not opposed to trying new stuff, but just throwing out some considerations

💯 3
❤️ 3
deactivateduser22:11:11

Yep - and I greatly appreciate it!

deactivateduser22:11:03

Ok just verified that (ns my.test!) in file src/my/test!.clj works, at least on MacOSX.

Alex Miller (Clojure team)22:11:46

if you just make all your namespaces pure, you don't need a naming convention

👍 3
deactivateduser22:11:50

I agree, but take a look at https://clojurians.slack.com/archives/C053AK3F9/p1605219494143300?thread_ts=1605218686.138600&amp;cid=C053AK3F9 for a concrete example of where impurity necessarily bleeds in.