Fork me on GitHub
#clojure
<
2023-10-06
>
Rupert (All Street)09:10:06

When building a library that is intended for a lot of use: (1) Are you for or against including "complementary functions"? (2) Do you think including them in clojure.core was a good or bad design decision? e.g. some functions have complementary functions: • some? / nil?when / when-notif / if-notfilter / remove= / not= • etc Others don't have one e.g.: • clojure.string/blank? Pros: • Extra expressiveness • Possibly more concise (e.g. (if-not x y z) vs (if (not x) y z) ) • Less nesting (e.g. (not= a b) vs (not (= a b)) ) Cons: • Extra functions to learn • Ambiguity (there's now 2 way of doing things!) (e.g. (not= a b) vs (not (= a b)) )

p-himik09:10:55

IMO those get complementary counterparts because of frequent usage. All in all, they are worth it. But not every function/macro is also worth it, far from it.

Rupert (All Street)09:10:08

So you draw the line based on "frequency of use". Doesn't that create a chicken/egg problem?: When designing a library the property of "frequent usage" is defined by the users of the library not by the library itself. I can neither force users to use my library frequently or not. So how does the library design to a constraint that isn't within its control or perhaps even within its visibility (e.g. proprietary code use)?

delaguardo09:10:21

clojure core mostly build on itself not on dispatch to java library. That could justify adding those functions on frequent usage basis, imho.

Rupert (All Street)09:10:35

@U04V4KLKC interesting - I hadn't thought about the performance aspect. Interestingly much of the complementary code is done in clojure not java so there is no performance benefit (e.g. remove is defined by calling filter). [https://github.com/clojure/clojure/blob/master/src/clj/clojure/core.clj#L2843]

(defn remove
  ...
  ([pred coll]
     (filter (complement pred) coll)))

p-himik09:10:58

> I can neither force users to use my library frequently or not. If you don't have any data on how frequently (not (f ...)) will be used a compared to (f ...) then don't add (not-f ...). If you gather such data later on and figure that not-f would be useful, you could just add it - it's a backwards compatible change.

Rupert (All Street)09:10:20

It's backwards compatible but you've left your early adopters + early blog posts + early documentation with "the old way" which is different from the new way. Which could confuse the new adopters. Also usage can dip in future - but taking the complementary functions away is not backwards compatible. So your rule of responding to observed usage only works in one direction (it's like an unstable equilibrium).

Rupert (All Street)09:10:36

So your rule becomes "complementary functions are ok if a library function is frequently used or is not frequently used but at some point in its history it was frequently used enough".

daveliepmann09:10:34

Do you have a particular example from a library you're developing?

2
vemv09:10:43

I think that "complementary functions" is too broad of a category to make a blanket statement about :) Are you creating something similar to clojure.core, clojure.spec, or something rather domain-specific? I'd say that the more specific the lib, the more surprised I'd be by an abundance of functions for 'expresiveness'

1
delaguardo09:10:03

> complementary functions are ok if a library function is frequently used this is fair even during library development. For example not= appears 6 times in clojure core

Rupert (All Street)09:10:52

An example would be a utiltiy library like https://github.com/weavejester/medley

daveliepmann09:10:09

(On the clojure.core question, yes they're useful and nice to have and the downsides seem completely insignificant. What widespread language lacks a not-equals?)

daveliepmann09:10:41

> An example would be a utiltiy library like https://github.com/weavejester/medley I mean a specific function, not a lib

Rupert (All Street)09:10:34

I often need the complement of clojure.string/blank? about 10 times a week. Sometimes I use (when (not (str/blank? x))) sometimes I use (when-not (str/blank? x)) so that's two ways of writing the same thing - would the world be a better place if we had a third option (when (str/non-blank? x)))? That's not counting (if (not (str/blank? x)) ..) and (if-not (str/blank? x) x) . Which is another two ways of doing the same thing and also takes less typing.

delaguardo09:10:46

making world a better place is up to us ) fortunately it is so easy to add a namespace with such function

Rupert (All Street)09:10:42

But it would be weird to have the complement function in a completely different namespace - I imagine other programmers on my team would just use (not (str/blank? x)) instead of requiring an extra namespace?

delaguardo09:10:04

that would be a case for any namespace you develop over time

vemv09:10:04

That particular example is a classic to me (I like to name that function present-string? or string/present?) But I see it as gradient - I wouldn't justify adding a complementary function to cases that didn't seem to have a pressing need.

👍 1
daveliepmann09:10:05

(when-not (str/blank? s)) reads well to my eye, and seems to illustrate why having lower-level complements prevents/minimizes the need for a proliferation of higher-level complements.

👍 1
1
Rupert (All Street)09:10:43

So if I'm writing a coding guide for my team to define when to write complementary functions or not, there seems to be no clear answer. Mostly don't write complementary funtions (YAGNI), but sometimes do write them (but this is dependent on frequency of use which you don't yet know because you don't have any users yet (except yourself))).

daveliepmann09:10:04

It seems highly context-dependent

Rupert (All Street)10:10:55

Thanks all for the input. Interesting to hear that having complementary functions in libraries are not totally frowned upon in some circumstances (ie when they deliver sufficient value to users to overcome the added complexity/sprawl). The thread has certainly helped me think about when to include / not include these functions.

Dustin Getz10:10:25

clojure is also fairly fragile , type tetris even very simple (flip compliment etc) falls apart easily, clojure does best when you can write brutally short code with as few points as possible. Flip in particular is completely absent for good reason, it shows up all the time in haskell yet in clojure we carefully design idioms like ->> to make refactoring errors stick out while on autopilot

Dustin Getz10:10:45

natural human language has similar characteristics, sound medium is such a low bandwidth info channel that we develop rich overlapping vocabulary to let us increase precision with few words. Unlike clojure, natural language also has built in error correction, something that i think would be neat and helpful in clojure

phill11:10:38

I don't know whether = vs not=, if vs if-not, filter vs remove were innovations in Clojure or borrowed from somewhere and therefore "expected". But the HOPL paper refers to efforts, early on, to make Clojure magnetic. The code clarity resulting from remove vs filter, the thrill-of-freedom-via-macros evident in if-not, might have figured in that calculation.

Joshua Suskalo15:10:08

the existence of if-not and when-not means that you only really need complement functions in higher order functions that themselves lack complements. My recommendation would be to prefer not writing complements, except perhaps for higher order functions which complement their arguments (like filter vs remove). This reduces how many functions need complements to remain expressive without "extraneous" code to create said complements in user code. In addition, I do not think that old code being littered with "the old way" is a problem here. Both ways of dealing with it are quite clear, and unless you get into situations with double or triple negatives I wouldn't worry about it. In general Clojure is not the language of There Is Only One Way To Do It like some other languages try to be. Trust your users to explore your api some and discover these potential idioms.

👍 1
seancorfield16:10:16

> Flip in particular is completely absent for good reason, it shows up all the time in haskell Interesting that you mention this function. I felt a need for it a couple of times so I added it to our "Clojure extensions" library at work https://github.com/worldsingles/commons/blob/master/src/ws/clojure/extensions.clj#L93-L100 but as code has been refactored over time, we just stopped using it. In fact, of the functions in that library we only use condp-> (in a dozen places), dissoc-all (in two places), and interleave-all (also only two places). The rest we've either never used or stopped using. Which makes me appreciate the design of clojure.core even more over time as I don't feel I've been able to come up with much that is missing -- in over 12 years of production Clojure.

Joshua Suskalo16:10:06

Personally I've found "partial->" to be more useful than flip

(defn partial->
  [f & args]
  (fn [x]
    (apply f x args)))

Joshua Suskalo16:10:03

As for what has felt "missing" to me, I think the primary thing has been when-pred, a way to conditionally flow a value through a function in the middle of a -> chain without needing to change the whole thing to a cond->, and tools for associng optional values into a map, e.g. assoc-some

seancorfield16:10:27

(-> some-expr
    (some-fn1 arg2)
    (cond-> pred-expr (some-fn2 arg2 args3))
    (some-fn3 2 3 4))
Not like that @U5NCUG8NR?

1
Joshua Suskalo16:10:06

more or less, except that it depends on the current value of the thing being threaded through, which can't be done quite like this.

Joshua Suskalo16:10:32

it's proved repeatedly unwieldy to make a thread chain based on the current value being threaded, which is why I made when-pred

seancorfield16:10:35

Ah, my condp-> macro above then... yes, and I guess that's why we still have a dozen uses of it in our code.

seancorfield16:10:02

Is your partial-> intended for use in -> chains? I suspect I would just reach for as-> in that case...

Joshua Suskalo16:10:43

no, it's intended for passing stuff to map that you wish you could use partial for, but for the fact that the argument you need to pass via map is the first one.

Joshua Suskalo16:10:00

hence why I compared it to flip

seancorfield16:10:00

Gotcha! I'd probably just pass #(f % the args go here) or (fn [x] (f x the args go here)) depending on whether I thought the arg name was important. partial-> implies an association with -> to me.

Joshua Suskalo17:10:40

Yeah, the only connection I could think of was #(-> % (f arg1 arg2)) at the time, which is why I chose the ->

simongray09:10:16

Can I make shuffle reuse the same random seed? I need random, but reproducible results.

delaguardo09:10:43

no, shuffle calls signle arity variant of java.util.Collections/shuffle and there is no way to alter this except writing another version of shuffle

magnars09:10:51

@U4P4NREBY

(defn shuffle [seed ^java.util.Collection coll]
  (let [al (java.util.ArrayList. coll)]
    (java.util.Collections/shuffle al (java.util.Random. seed))
    (clojure.lang.RT/vector (.toArray al))))

👏 1
simongray10:10:53

Thanks, everyone!

daveliepmann10:10:21

might be worth an ask.clojure post? other folks might appreciate the additional arity

magnars10:10:26

Note that java.util.Random has some issues with too low seeds. This is solved with java.util.SplittableRandom which is a drop-in replacement in the code above.

👍 2
💡 1
simongray10:10:45

@U05092LD5 yes, you may be right about that.

delaguardo10:10:48

imho, it is better to pass java.util.Random instead of a seed

dpsutton12:10:24

When I’ve faced this I try to come up with a fingerprint of the collection (turn it into a map with some keys) or other properties of the collection that I can assert on. Order of a randomized thing is hopefully something you might be able to ignore

simongray12:10:50

@U11BV7MTK I need to create a randomized dataset and it needs to be reproducable because it’s going into a research paper.

delaguardo12:10:47

you can generate and save it as edn.

simongray13:10:03

Well, I am saving it of course, but the point is giving people the option run the code in order to verify.

dpsutton13:10:59

That’s a fantastic reason to shuffle with a known seed

😄 1
💯 1
chrisn13:10:10

both take :seed which can be either integer or instance of java.util.Random.

Noah Bogart14:10:41

there's a 9 year old JIRA ticket with a patch for making the clojure.core random functions take a seed: https://ask.clojure.org/index.php/4257/clojure-core-rand-for-seedable-randomness

👏 1
😅 1
simongray14:10:05

Here’s the solution I went with. I am calling shuffle hundreds of times, often with the same coll, so I expect different results every time, but I ALSO want reproducibility. For this reason, I went with an incrementing random seed.

👍 1
simongray14:10:30

(I will probably sort the coll in some way too before handing it over to sample in order to get rid of my own annoying TODO)

dpsutton14:10:13

is it possible this introduces some significant non-randomness and interferes with the properties of your sample?

simongray14:10:42

not from what I can see

👍 1
chrisn17:10:37

Ensuring that coll is https://cnuernber.github.io/ham-fisted/ham-fisted.api.html#var--.3Erandom-access will speed up repeated calls to shuffle :-).

simongray18:10:53

Thanks @UDRJMEFSN, though it's not really a great concern for me for now, since the code runs pretty fast as-is. Probably >95% of the time is spent running various SPARQL queries to fetch the raw data.

👍 1
1
Ingy döt Net17:10:58

=> (->> "(foo bar true false nil)" clojure.edn/read-string (map type) pprint)
(clojure.lang.Symbol
 clojure.lang.Symbol
 java.lang.Boolean
 java.lang.Boolean
 nil)
are true false and nil the only symbol-like words that edn doesn't read as symbols?

Kiyanosh Kamdar17:10:07

Hello, We have a monorepo with one common deps.edn that contains all dependencies and versions. We then run a script, that replaces a deps.edn.template file in each subproject with the “common” established version found in the one common deps.edn file. I would like to remove this step and eliminate the need for the script and template file. Can this be done? I would like to do something similar to Maven. Where inside the subproject deps.edn file I can specify the required dependency as such com.stuartsierra/component {} and the version number can be found from the common deps.edn file. I read through the monorepo blog post, and the tools.build docs, but nothing popped out.

p-himik18:10:26

You can move all the common dependencies into, say, project/common/deps.edn, and then add project/common {:local/root "../common/deps.edn"} to every subproject's deps.edn.

seancorfield18:10:53

If you're will to use aliases, you could use :override-deps which will "force" the version for those deps if present. An override for a dep you don't use in a specific project is just ignored.

💡 1
seancorfield18:10:37

(you can only use :override-deps in an alias)

Kiyanosh Kamdar19:10:48

Thank you so much. But we don’t want to pull all the dependencies in the common. The project just wants only the dependencies it cares about. But the version coming from common. So for example common deps.edn could have 100 dependencies listed, but my subproject only uses 2 of them. But the version will common from the common deps.edn

Kiyanosh Kamdar19:10:59

hmmm looks like the :override-deps might be thing I need.

Kiyanosh Kamdar19:10:14

@U04V70XH6 would you have an example in github?

Kiyanosh Kamdar19:10:59

Actually, the override may not work, the way we intend.

seancorfield20:10:57

@U03EEEB90DV Have a read through this series of posts: https://corfield.org/blog/2021/02/23/deps-edn-monorepo/ -- we were facing similar issues and went through several iterations before we got something satisfactory (and we ultimately moved to Polylith which solved a lot of other issues -- but still requires duplication of dependencies, which we decided was an acceptable trade off in the end).