Fork me on GitHub
#clojure
<
2024-04-27
>
Ian Fernandez05:04:59

I'm interested in a library for building a graph of functions dependencies between namespaces and inside the namespace, is there any tool available for this that you folks recommend me? Thanks 😄

tomd07:04:07

clj-kondo can do this. I've used https://github.com/benedekfazekas/morpheus to visualize that data in the past, with some success. Worth a look.

👍 1
john16:04:53

reproduce - Most similar to reduce but with a simpler api like map

(defn reproduce [afn acoll]
  (reduce afn (empty acoll) acoll))

(->> [1 2 3 4] (reproduce #(conj %1 (inc %2)))) ;=> [2 3 4 5]

(->> #{1 2 3 4} (reproduce #(conj %1 (inc %2)))) ;=> #{4 3 2 5}

(->> {:a 2 :b 4} (reproduce (fn [m [k v]] (assoc m v k)))) ;=> {2 :a, 4 :b}
Comments?

Noah Bogart16:04:45

seems unnecessary? if you find it helpful, then go for it, but i don't ever struggle with reduce or into or any others. the fact that you have to manually write the conj makes me think it's not worth the effort

john16:04:47

Yeah, if you didn't want control over the conj you'd probably just use transducers

phill16:04:23

Could you just use (empty acoll) in place of the condp?

🙏 1
hiredman16:04:24

(transduce (map f) conj (empty x) x)

💯 2
john16:04:30

Yeah, I'd probably never use this, but something just bugs me about reduce.

john16:04:52

Compared to map

john16:04:16

map just feels more elegant and I want a more elegant version of reduce

Noah Bogart16:04:17

what don't you like about reduce?

john16:04:43

but yeah I'd probably never use this in a team setting, unless everyone was using it

john16:04:56

It's completely subjective

john16:04:21

The ambiguity around the 2 arg vs 3 arg version

john16:04:55

Something about the mental model is harder for me than map, more like a loop, on the scrutability scale

hiredman16:04:03

Oh, of course (into (empty x) (map f) x)

1
john16:04:14

ah right

hiredman16:04:55

into will even use transients internally

john16:04:00

but the user has access to the acc the whole time, so they can do stuff that reducers do there

john16:04:23

map needs to return nils

hiredman16:04:29

The only thing reproduce does differently from reduce is fix the accumulator

john16:04:33

but yeah it can probably be done in pure transducers

john16:04:59

it gives the user access to update the acc in downstream items

hiredman16:04:12

Reduce also does this

john16:04:38

so this is like a reduce with training wheels. Suppose to feel simpler, like map

hiredman16:04:07

map is reduce

john16:04:16

yeayayea

john16:04:53

but map returns nils, whereas sometimes you really do want a reduced collection, but without all the reduce ceremony

hiredman16:04:03

dunno what "map returns nils" means

john16:04:23

You can't return a collection of a smaller size with map

john17:04:06

does keep let you change the kept value?

john17:04:45

so more like filter

john17:04:58

Does reduced work in map?

john17:04:01

I guess that doesn't make sense

john17:04:51

If I had a really big code base on a solo project I'd give it a try to see if it's useful but yeah doesn't seem useful enough to be its own thing at first blush

phill17:04:05

keep is super unless false is an important, significant return value from f

john17:04:26

or nil, right?

phill17:04:33

keep discards all falsey results

john17:04:13

reduce lets you build colls with falsey vals

john17:04:36

or choose not to

john17:04:19

few things besides reduce give you both options

john17:04:56

other than loop

john17:04:28

hmm maybe this is cleaner, and easier to work with transducers:

(defn reproduce [afn bfn acoll]
  (condp = (type acoll)
    cljs.core/PersistentVector
    (reduce #(afn %1 (bfn %2)) [] acoll)
    cljs.core/PersistentArrayMap
    (reduce #(afn %1 (bfn %2)) {} acoll)
    cljs.core/PersistentHashSet
    (reduce #(afn %1 (bfn %2)) #{} acoll)
    ; :else
    (reduce #(afn %1 (bfn %2)) () acoll)))

(->> [1 2 3 4] (reproduce conj inc)) ;=> [2 3 4 5]

(->> #{1 2 3 4} (reproduce conj inc)) ;=> #{4 3 2 5}

(->> {:a 2 :b 4} (reproduce merge (fn [[k v]] [v k]))) ;=> {2 :a, 4 :b}

john17:04:47

as an api

john17:04:00

Where the "reducing function" has access to the whole coll on each step, so the user can still choose to reduce or add to the coll at each step

john17:04:45

mm'no, afn needs access to %2 too make that decision, so maybe it might as well be a single fn :thinking_face:

john19:04:13

@U0HG4EHMH oh, right, with the (empty acoll)

(defn reproduce [afn acoll]
  (reduce afn (empty acoll) acoll))
easy

john19:04:53

I'm going to update the opening post with that since it makes it easier to understand

john20:04:25

Hmm, with that api, you can't do stuff like (reproduce + [1 2 3]) ;=> 6 This is maybe better:

(defn produce [afn acoll]
  (reduce afn nil acoll))

(produce + [1 2 3]) ;=> 6
(->> [1 2 3 4] (produce #(conj (or %1 []) (inc %2)))) ;=> [2 3 4 5]
(->> [1 2 3 4] (produce #(conj %1 (inc %2)))) ;=> (5 4 3 2)
(->> #{1 2 3 4} (produce #(conj %1 (inc %2)))) ;=> (4 3 2 5)
(->> {:a 2 :b 4} (produce (fn [m [k v]] (assoc m v k)))) ;=> {2 :a, 4 :b} 

john20:04:16

And you can just do a (into #{} ... after the produce if you need a set afterwards, like we do with mapping functions

john20:04:28

In which case the name should probably just be produce, since the original collection type isn't being reproduced anyway (updated above example)

john20:04:41

The idea is just basically a simpler reduce, that starts you off with simpler semantics, before you learn about reduce

craftybones01:04:19

(produce + [1 2 3]) doesn’t work here.

craftybones01:04:59

This seems like a strange hybrid of into, reduce and map. I am not sure if the semantics are necessarily simpler. It isn’t equivalent to reduce, which means, if you wish to use produce to generate anything other than a collection, then you will have to learn about reduce anyway.

1
craftybones02:04:36

Is reduce really that hard to learn?

craftybones02:04:35

I use this to teach reduce and it seems to work well enough

(-> init
    (f first-element)
    (f second-element)
    (f third-element)
    ...)

john02:04:57

I mean it's learnable. Just not as smooth as map

john02:04:55

produce has a uniformity to it that makes it easier to reason about longer term iterations IMO

john02:04:22

Just not as useful as reduce

john02:04:48

I also don't want to waste my time scanning my eyes all the way to the end of a reducing function just to see if/what first param it's taking (I already know what's flowing on the right hand side of the thread-last pipeline). With tools like produce you can signal to the reader that this only takes a single sequence argument after the mapping/reducing function, like most other things in your thread-last pipeline

john02:04:56

Don't make the reader's eyes backtrack, if possible. These reductions are just easier scan with your eyes fast down a thread-last form:

(->> #{1 2 3 4 5 6 7 8 9}
  (produce #(conj %1 (inc %2)))
  (produce (fn [m n] (+ m (if (even? n) n 0))))) ;=> 30

john02:04:16

Not sure why (produce + [1 2 3]) ;=> 6 wasn't working for you

john02:04:08

Oh, maybe it works in cljs and not clj

john02:04:44

Yeah, + is different

john02:04:44

Yeah, I guess it wouldn't be a subset of reduce in that regard :thinking_face: hmm

john02:04:04

Yeah it could just be a thing for sequences, but yeah I think that makes it less interesting

john02:04:30

So it's back to this original thing that reproduces the source type:

(defn reproduce [afn acoll]
  (reduce afn (empty acoll) acoll))
(->> [1 2 3 4] (reproduce #(conj %1 (inc %2)))) ;=> [2 3 4 5]

john11:04:45

Maybe it should be called induce, since it only reduces what's in the collection but not the outer collection type 😆

john11:04:08

I wonder how often, in the wild, the source and target collection types of reduce calls are necessarily different :thinking_face:

john11:04:52

Well, you can change the return type half way through the reduction, if you wanted to. It just starts with the type you had

phill13:04:02

Sounds more and more like the kind of feature they need in Python

daveliepmann16:04:08

> I wonder how often, in the wild, the source and target collection types of reduce calls are necessarily different :thinking_face: A significant minority for me/codebases I work in

daveliepmann16:04:44

> The ambiguity around the 2 arg vs 3 arg version [is inelegant & bugs me] Reminds me of Rich denouncing the 2-arg arity in https://github.com/matthiasn/talk-transcripts/blob/master/Hickey_Rich/InsideTransducers.md. When given the opportunity to try again, he put the onus on f instead: > Who knows what the semantics of reduce are when you call it with a collection and no initial value? No one, right. No one knows. It's a ridiculous, complex rule. It's one of the worst things I ever copied from Common List was definitely the semantics of reduce. It's very complex. If there's nothing, it does one thing. If there's one thing, it does a different thing. If there's more than one thing, it does another thing. It's much more straightforward to have it be monoidal and just use f to create the initial value. That's what transduce does, so transduce says, "If you don't supply me any information, f with no arguments better give me an initial value."

👍 1
john16:04:26

Interesting. Yeah, it's unfortunate I guess. Rich prolly won't impose a new version of reduce on the community, so if the community doesn't adopt and experiment with new idioms then the core team will be disincentivised from causing the imposition of the new semantics. Like a better-reduce or something

john16:04:33

It's a bit of a catch 22, trying to grow the core semantics, when what we have is "good enough"

john16:04:39

Which has it's obvious benefits as well

daveliepmann16:04:10

I wonder how often reduce calls in the wild use the 2-arg version :thinking_face:

john16:04:11

Yeah same

john16:04:19

Is there a library out there that allows you to add functions to clojure.core globally for the whole project? Maybe poly-filling community ideas into core would make community experimentation easier and have less friction

john17:04:43

Cause honestly nobody is going to proactively litter every namespace across their whole project/lib/app with :refer [induce]

john17:04:35

The js community seems to be able to do polyfills without everything falling apart. We could probably do it better, given the circumstances

daveliepmann17:04:36

Sounds like a util ns, which I'd vastly, enormously prefer over magic requires

john17:04:35

It's more like, "I'm the lead dev at acme widgets and I want our team to join two other companies in trying new thread-last semantics across all of our apps and projects" and then those companies could report on the results of the experiment

john17:04:55

We could litter our namespaces with this experiment in what we might consider a core function of the language, with organizational rules, "remember to require this stuff into all your new namespaces, thx!!!" But that obviously sucks

john17:04:14

And so no real experiments in core-ish functions like induce ever really happen because there's just too much friction for those kinds of experiments

john17:04:15

They might happen at clojure.core, but then the community just becomes passive consumers of clojure.core's preferences, but, again, they're disincentivised from that kind of experimentation, so community market effects are such that some notional induce, even if it's a good idea and should replace reduce in many circumstances for clarity's sake, will just never be adopted. There's too much friction for all parties involved to get something like induce adopted

daveliepmann17:04:33

> that obviously sucks i wonder what other folks do because i don't find it that onerous

john17:04:26

I mean, in a healthy system, I think the clojure core team should be seeing more inspiration from the community for new ideas. It should be easier for the community to incubate ideas and for the core team to be able to say, "yup, we want that," and be able to integrate an idea that they can already see work in example prod environments. That happens with libs, just not as much for one off core functions

john17:04:10

It's not onerous normally. I'm talking about the kind of function you want to be globally available, as a kind of language wide dev UX experiment

daveliepmann17:04:15

> no real experiments in core-ish functions does run! not count as one of these?

john17:04:50

Not familiar with it's backstory

daveliepmann17:04:51

> I'm talking about the kind of function you want to be globally available I am too, or at least I think I am

daveliepmann17:04:08

Just that it's a core fn, very similar to reduce and map, introduced recently

john17:04:20

Did the community inspire that change? Was it battle tested in libs in the wild before the core team brought it in?

john17:04:39

Not that things have to be. And to be clear, I think we're better off that things don't work that way for the most part. Most people ought not be messing with polyfills

👍 1
daveliepmann17:04:39

at least, not "in the wild" meaning a contrib-style system of community creating something which graduates to core.

john17:04:50

So yeah, we have the incubator repos. Some of them are gems. And that serves it's purpose for those kinds of features.

john17:04:26

But then you have these "kitchen sink" libs that have these awesome little functions, but you know who's not going to bother trying that kitchen sink lib, in every namespace, just so I can try out induce, whether it's in the incubator or not? Me. Too much friction

john17:04:23

However, if it's just a matter of adding the lib to my deps (and maybe having to require it in one of the namespaces) then I think it wouldn't be too much bother for an organization to do organization wide

john17:04:00

And most of the devs never have to worry about whether it's required or not

john17:04:07

Like letting users create an origin trial, test it in org, without having to fork clojure just to report back to the community and the core team on the results of the experiment

john17:04:19

And of course the core team is under no requirement to even look at your experiment. I'm just saying that conducting the experiment seems like a lot of friction right now

john17:04:11

I guess enforcing an organizational wide user file that make some functions globally available would be another way

john17:04:05

Reader tags work this way though, so there is some prior art

john17:04:35

Require it in one namespace and it'll work across the whole project

john17:04:17

In cljs you don't even need to require it, just add the dep

john17:04:38

This is specifically about "core functionality" though, usually pertaining to developer UX, which is the only reason you'd want to go around the ns form to require it in

john18:04:56

Like, imagine 100 years from now, some clojurists are talking and one says, "you know, back in the day, Rich did say that reduce kinda sucked." So someone responds, "Fine, let's all agree to start using something better starting now." So folks start experimenting with different things but between the years of 2120 and 2130, every namespace everywhere was littered with tons of different implementations of new-reduce and my-reduce and, in the end, nobody could agree on a new reduce because people were more fed up with the polluted namespaces than they were happy with the new thing, so it never got added and clojurists were doomed to an eternity of never improving on reduce. That would be post apocalyptic lol

daveliepmann18:04:36

Isn't that precisely what happened with Common Lisp, and Clojure was the solution?

john18:04:05

In some ways, not others

john18:04:25

Reader tags hasn't CLified things

daveliepmann18:04:10

the drawbacks of reduce are extremely minor and well-solved by just...not using the 2-arity version

john18:04:23

JS is clearly getting the polyfill thing right, as a tool talk about the future

john18:04:08

That may be true, but does it have to be true in a million years?

john18:04:28

When can we finally have something better than what rich admitted was bad?

daveliepmann18:04:13

there are multiple solutions to the stated problem • ignore it • deal with it case-by-case • linter warns on 2-arity usage • magic global requires • clojure 2.0 • clojure plays the role of CL as new lang supplants it Of those, I might like magic global requires the least

daveliepmann18:04:42

it's also very weird to me to fix "reduce is complex because of its 2-arity" by introducing complexity at the ns/require level

john18:04:20

Don't you think the best option would be: * the community slowly adopts TechA, it grows steadily until core team decides it belongs in core Right now, that path is hard because reaching critical mass on adopting core idioms involves too much friction

john18:04:45

Isn't that the healthiest adoption pipeline? People bang on ideas and the core team gets to bring in the best parts

john18:04:51

Right now, core team just has to have extreme insight to know a priori what ideas will be good and bad in the wild

daveliepmann18:04:52

that sounds like a social issue, not a technical one

john18:04:09

Can they really know that induce is a good idea? What if it's mostly good for beginners? There's no system for trying things out on larger test groups with less friction

daveliepmann18:04:21

as opposed as I am to magic global requires, I am enthusiastic about tools/environments which provide racket-style language levels for clojure. the social/technical context there is completely different.

1
john18:04:31

I guess I'm imagining a scenario where the core team can facilitate language level experiments with some polyfill system, while not imposing any requirement or expectations on the core team to pay attention to any particular polyfill/trial

👍 1
john18:04:06

How does JS avoid the CL fragmentation issue with their polyfill? They ship the polyfill in the lib, right? Do js devs ever yell about how they got bit by a polyfill under their stack that they didn't know about?

john18:04:24

I guess the risk is lower since js doesn't have macros

john18:04:01

And the cl fragmentation issue is really a social one, of unrecognizable syntax

john18:04:34

Whatever the case may be, polyfills are working for js so we must be making mountains out of molehills here with this particular aversion

john18:04:53

But a polyfill in js essentially is a macro

daveliepmann18:04:13

> And the cl fragmentation issue is really a social one, of unrecognizable syntax not sure I agree — plenty of the fragmentation was behavior in competing libraries which we currently enjoy in core, e.g. seqs and other abstract data structures, i think threads, that kind of thing

daveliepmann18:04:30

i mean i agree it's social 🙂

john18:04:12

Agreed (though I'm foggy on the CL history)

👍 1
john18:04:36

Another example, I have this injest library that adds +>, x>>, some others, that mostly introduce new semantics to the existing thread macros. There's a few other libs out there doing similar threading things and we're running out of names! And aliased thread macros suck (`(clunky/-> ...`). And maybe we want to test some new semantics for the core ->, maybe just adding some validation, disallowing literal function definitions as top level forms in a thread, and we've run out of cool new thread operator names and we resigned ourselves to shadowing ->. If 25% of the devshops out there adopt safe-> transparently for their -> then core can have an idea of what people are finding useful in core without having to know about the usefulness of everything beforehand. Right now, there's too much friction for 25% of devshops to adopt safe-> transparently

phill20:04:08

The produce/induce discussion reminds me of Jozef Wagner's fork of Clojure: Dunaj (https://groups.google.com/g/clojure/c/af4mqG8TzPs): which was elegantly introduced in a series of 12 essays, one about each area of extension or experimentation, and subsequently, as far as I know, magnanimously forgiven, both the good and the bad. Because, imagine the shot-in-the-foot to the ecosystem if code were less portable... Well, I took Wagner's Dunaj in the vein of delightful speculative fiction. But some grass-roots experimentation already happens, outside the supervision of Clojure's keepers, for good or ill, without the social disruption of an outright fork, and the experiments have users whose opinions could be studied because they opted-in on the basis of other factors. The quantum wormhole that's been discovered to these experimental gardens is: a new platform. ClojureScript, ClojureDart, and YamlScript all have some more-or-less bold variances from Clojure. (All right, YamlScript takes the cake... S-expressions improved with infix operators as "Yes-expressions".) So... in a nutshell... If you would like to put @puzzler’s better-cond in core, or a novel twist on ->, or constants evaluated in case like they are in ClojureScript, or set operations that throw if it isn't a set, just roll up your sleeves and take charge of a port. Simple!

john20:04:39

Yeah, a lot of better-foos have come and gone. Again, we don't want better-foo if it means bringing in kitchen.sink.utils every time we want to use it. It's just not worth it. So clojure/script doesn't end up with SDKs like jQuery. Either we use core or we just build it locally bespoke. To bring in a lib, it has to do a lot of what I don't want to do myself. So kitchen sink libs are never really successful in clojure, containing a bunch of little things that most of us individually aren't interested in, so we never actually end up giving better-cond a shot. Like it never really had a chance, even if it really was better, because we just don't have enough desire to include it in every single namespace, in every project, forever into the future. It's too much friction

john21:04:38

It'd be nice to accommodate optimizing compiler plugins through some polyfill thing too. Why not let the community incubate optimizing strategies that may never make it into core? Somebody is going to write the optimization layer one day anyway, right? But yeah, I'm not saying core should pay any attention to any given experiment. I just have very little incentive as a clojure developer to participate in any experimental libraries that try to do something better than core. It's too much of a penalty for my downstream users and team members for me to impose those experiments on downstream users. Stepping outside of clojure.core just isn't worth it, given the economics of things.

john21:04:34

I'm pretty excited about injest, for instance. I tested it on an app servicing a primary part of the company I was working for and in the end I decided against bringing injest into the app. For that particular app, it only brought us nominal performance improvements. Unless it's absolutely necessary, it's technical debt, so I didn't think it was worth the educational overhead for all future developers of the app to impose the new semantic. So of course I'm never going to bring induce into a production app that I promise will be easily supported even after I'm gone. But I'm saying that's a problem. I should have more incentive to buy projects into less vanilla idioms. Evolution should be less costly, somehow

john21:04:21

I probably wouldn't have opted that app into any experiments anyway, too important, so maybe not a good example. For internal facing apps though, sure

john21:04:31

Idk, I don't have the right answers. I'm just surprised how well polyfills are working out in the js world though and I'm curious if we could benefit from it somehow

john21:04:18

Well, it's not as if we need permission, I'm sure polyfill type things (monkey patching clojure.core, for instance) can be done in libs, and devs don't need permission to do so. It's just not something we do

Alex Miller (Clojure team)01:04:14

> I mean, in a healthy system, I think the clojure core team should be seeing more inspiration from the community for new ideas. It should be easier for the community to incubate ideas and for the core team to be able to say, "yup, we want that," we do have this - you file a question at https://ask.clojure.org and the community can then vote on it. We look at the top-voted things in every release and use that as one important signal for what we work on. We may ultimately decide not to include it but we are actively considering things here by vote.

Alex Miller (Clojure team)01:04:57

there is very little reason to monkey patch clojure.core (and that really seems even detrimental to you if at some later point core adds the function). We have a whole namespace system - when you need a function, include the namespace and use the function.

john01:04:05

Yeah, I don't mean to imply otherwise. And I may be asking for the impossible. I kinda want the conservativeness and exciting new stuff at the same time

john01:04:21

Yeah, that's true. But what about polyfills? Would you say JS polyfills are largely obviated by macros? I think polyfill experiments show pretty powerful language level experiments

Alex Miller (Clojure team)01:04:21

you have it - make all the namespaces with all the functions and macros you want

Alex Miller (Clojure team)01:04:45

there is very little you truly need language level change for in Clojure

john01:04:56

Yeah I agree

Alex Miller (Clojure team)01:04:54

and those few things probably have 10x-100x impact than you realize

john01:04:32

I think my larger point too was that just culturally we're just super averse to letting some new better-cond or induce kind of new semantic into our codebases

john02:04:15

I know I am

john02:04:56

So I don't know how we'll ever end up with "better reduce" you know? But maybe it's for the best

Alex Miller (Clojure team)02:04:59

there is no barrier to do so - they are just functions. we have added things from clojure utility libraries in the past (`update-keys` and update-vals are variants of things that existed in many libs, for example)

john02:04:23

Yeah, those are good examples

john02:04:36

Those were in tons of libs, right?

Alex Miller (Clojure team)02:04:37

those came about because ask clojure questions were filed, they got voted up to the top, we evaluated, and added them

john02:04:07

Right, which is an awesome pathway. And evolution does happen

john02:04:04

I'm not trying to complain that y'all are doing something wrong or something. I'm just wondering how we can accelerate innovation in general. But Clojure does evolve, so it's not like I'm saying there's some systemic problem

john02:04:30

And I mean innovation in the community space, not necessarily in core

john02:04:26

And when talking about this induce fn, it dawned on me that even if it was good, there's pretty much no way we'd adopt it as a community, just because of cultural reasons. I'm disensentivized to produce too many idioms that compete with core idioms, as that eats into my own lunch in many ways. So unless y'all forced something like induce on the community, the community will probably never adopt such a core idiom that directly competes with reduce. I doubt we'd even vote it in if we could, even if it might benefit beginners, for instance. And I'm not actually sure there should be an induce like thing in core. It's just an example

john02:04:54

And it's interesting because reduce is one of those examples where even Rich agrees that there could be a better version out there, but we're still stuck with semantics from developer inertia going all the way back to CL

phill20:04:08

The produce/induce discussion reminds me of Jozef Wagner's fork of Clojure: Dunaj (https://groups.google.com/g/clojure/c/af4mqG8TzPs): which was elegantly introduced in a series of 12 essays, one about each area of extension or experimentation, and subsequently, as far as I know, magnanimously forgiven, both the good and the bad. Because, imagine the shot-in-the-foot to the ecosystem if code were less portable... Well, I took Wagner's Dunaj in the vein of delightful speculative fiction. But some grass-roots experimentation already happens, outside the supervision of Clojure's keepers, for good or ill, without the social disruption of an outright fork, and the experiments have users whose opinions could be studied because they opted-in on the basis of other factors. The quantum wormhole that's been discovered to these experimental gardens is: a new platform. ClojureScript, ClojureDart, and YamlScript all have some more-or-less bold variances from Clojure. (All right, YamlScript takes the cake... S-expressions improved with infix operators as "Yes-expressions".) So... in a nutshell... If you would like to put @puzzler’s better-cond in core, or a novel twist on ->, or constants evaluated in case like they are in ClojureScript, or set operations that throw if it isn't a set, just roll up your sleeves and take charge of a port. Simple!