Fork me on GitHub
#clojure
<
2023-02-06
>
zane16:02:03

In keeping with https://stuartsierra.com/2016/01/09/how-to-name-clojure-functions, how do people typically name functions that non-destructively modify a value? I’ve considered with-<changes>, but I’m curious what other people typically use. For example, something like: (-> (cat/new) (cat/with-name "Henry") (cat/with-breed "Tabby")) I’m specifically wondering about the case where you have a good reason for not just using a map + associative collection manipulation functions.

zane16:02:01

with-<changes> mirrors clojure.core/with-meta. Or at least that’s the idea.

Alex Miller (Clojure team)16:02:02

with- is more often used as a macro that binds around a body

zane16:02:12

Yeah, that was one worry of mine.

Alex Miller (Clojure team)16:02:17

with-meta is an exception there

p-himik16:02:09

In my projects, I use set-*. Might use assoc-*.

👍 2
Alex Miller (Clojure team)16:02:21

is there some reason you need this function at all?

Alex Miller (Clojure team)16:02:52

I try to avoid writing things that are just "getter" / "setter" style fns and lean on clojure core for those

p-himik16:02:54

It helps with nesting, long keys, and overall tangle of assoc et al if you have it. As per the talk by Eric Normand: https://www.youtube.com/watch?v=Sjb6y19YIWg

zane16:02:47

Well, one example that came to mind is where the setter needs to be polymorphic. i.e. You don’t know that the underlying value is going to be a hash map / implement the associative interface.

zane16:02:00

Another example might be if you want to attach validation to the function.

vemv16:02:16

I'd say it also depends on what (cat/with-name "Henry") does if with-name merely assocs a specific key, you're better off simply using assoc. rich would be happy as he warns here and there about Java programmers' reimplementation of generic data structure manipulation ops. If you are worried about making typos/etc around the choice of key (i.e. you want to encapsulate the specific choice of the :name keyword), strategic use of Spec/Malli would seem more idiomatic. If it's not about assoc but some other arbitrary operation, I'd say most of the times one can use a verb. e.g. defn modify , not defn with-modification .

2
jjttjj16:02:05

just my opinion but I don't mind with-* for the usage you describe. I think the context can make it clear when it's a binding macro or not. I remember preferring this wording over set-* for https://github.com/scicloj/wadogo (which is a d3-like library, which uses the getter/setter style)

2
👍 2
zane16:02:18

> If it’s not about assoc but some other arbitrary operation, I’d say most of the times one can use a verb. e.g. defn modify , not defn with-modification . @U45T93RA6 This is great when it works, but it’s hard to image what the verb is for both name and breed above.

vemv17:02:07

Leaving the point about assoc 's genericity aside, perhaps an ideal name would be e.g. add-name , assign-breed etc which are still "verby" (the verb is emphasized as it's the first word) and gives a hint of what it's being accomplished I don't think with-name is as expressive as add-name . Unless you set out to create a convention around it

2
zane17:02:39

assign-* is interesting. Not clear if if there’s a principled argument for it over set-* or add-*.

vemv17:02:59

oh it's just an arbitrary word that came to mind because it seemed close to breed . Similar to the phrase "the gender one is assigned"

2
daveliepmann17:02:01

On a recent project I had a bunch of functions which were basically "`assoc` with enough extra work that a function is worthwhile". There were no natural names readily available and no synthetic ones worth creating. Naming them assoc-foo, -bar, etc. worked fine. If the function is essentially assoc-like then I would greatly prefer using that name over set- or with- since those too have specific connotations I don't want.

2
👍 2
jeaye17:02:07

Currently tinkering with a gameboy emulator in clj. The cpu is a map, but the logic for setting registers is non-trivial, so I have set- and get- fns. Using assoc- didn't feel right, since I don't want to assoc the high B register; I want to set it. Seems like leaky naming.

jeaye17:02:35

In general, I think it makes sense to name the fn based on what it logically does, not how it does it. For example, I'd use add-address over conj-address, too.

👍 2
2
daveliepmann17:02:38

Your "CPU map" seems like a situation where I'd prefer your approach, @U4986ECDQ.

didibus17:02:36

In Clojure, I would expect all functions to be non-destructive to its input, unless it ended with a !

2
jeaye18:02:27

For sure.

slipset19:02:29

I’m curious about the whole example here, (-> (cat/new) (cat/with-name "Henry") (cat/with-breed "Tabby")) could easily be expressed as: {:name "Henry" :breed "Tabby"} So I guess perhaps it was an example and not the actual case. From our codebase, I think we try to write fns that do more than just bang on maps, where I believe core fns are appropriate. The problem for me is if you do (assoc some-cat :breed "whatever") outside of the cat ns, if that makes sense

slipset19:02:47

In your cat ns, you should probably have fns which reflect what you do with a cat in the domain you’re modelling.

2
zane20:02:15

@U04V5VAUN Yeah, another thing I’m uncertain about is how often people do what you’re describing (all cat manipulation happens via functions in the cat namespace). It seems nice in the sense that you can change the underlying data representation of cats easily, but it doesn’t seem that popular of an approach?

zane20:02:46

i.e. Wrapping the domain object you’re modeling with an interface / abstraction.

slipset20:02:44

We see a need in our code base (80KLOC/60KLOC tests) as we’re growing the team to put in more and more interfaces, to a point where what used to be ok, like constructing a cat with {:name "Henry" :breed "Tabby"} from where-ever in the code base was ok, is now not so great because suddenly we also require that eg :gender is present which would have been easy to fix/find if we had a constructor fn, but now it’s much harder to see where we construct cats.

2
zane20:02:34

Yeah, that’s intuitive to me. I bet the same principle applies for functions that manipulate cats.

zane20:02:00

e.g. If naming already-constructed cats is common, and you want to do some validation on names as they’re assigned.

slipset20:02:10

And, perhaps, as you grow, not all strings are valid names for cats,

zane20:02:15

Exactly.

Alex Miller (Clojure team)20:02:22

I think writing constructors is usually fine, but getter/setters that do nothing more than shield you from keyword names is an antipattern in Clojure (this is a common go to for people coming from oo). The whole point of direct manipulation / keyword access is to remove boilerplate like that. I concede that more is needed when it grows beyond a few heads but usually schema libs that document/enforce/validate/generate are still a better answer than assoc-name or set-name (unless those are doing something more)

2
👍 2
2
slipset20:02:05

Perhaps, though it is nice to use namespaced keywords so if you in the future would need to encapsulate setting of name, you could more easily find all usages of ::cat/name than just :name or is there some down side to that as well that I don’t see yet

slipset20:02:55

I guess if you want to rename arbitrarily without knowing if it’s a cat or a dog, it’s nicer with :name than ::cat/name

Alex Miller (Clojure team)20:02:55

Using aliases auto referred keywords is a good way to use good kw names with less typing

2
didibus21:02:48

Since domain modeling was brought up. I think that's important, you have to decide if you simply need CRUD, or you need some strong Domain Modeling more in-line with DDD. If the latter, an operation like add-key doesn't make sense, you need to think in terms of the domain from the point of view of a domain expert. If say you have a Cat, and your domain are kids playing with cats, you might want operations like cat/pet, cat/feed, cat/cuddle, cat/clean-ears, etc. Your users are not exposed to CRUD operations directly, they are given higher level conceptual interactions with the entities, that uses the language they are used to use in real life. If you're doing CRUD, that's when you'll have things that are more like add-key, remove-key, because your users are exposed the CRUD operations directly, they create, remove, update or delete entities directly. Finally, these would be used from your users, but then you have internal functions that might be meant for devs, like cat/clean-ears might need to set the :ear-dirt-amount key/value from the Cat entity map to a new value. This is where I feel having setter/getters isn't useful, just use assoc in the implementation of ear-dirt-amount directly. But if you were giving a CRUD interface, you might want to have a update-ear-dirt function that the user can use through a user command.

didibus23:02:48

I like doing this personally:

(s/def : string?)
(s/def : #{:black :blue :grey :white})
(s/def :? boolean?)

(s/def :myapp/cat
  (s/keys :req-un [:
                   :
                   :]))

(defn assert-cat
  [cat]
  (if (s/valid? :myapp/cat cat)
    cat
    (let [explanation (s/explain-str :myapp/cat cat)]
      (println explanation)
      (throw (ex-info "Can't make the requested cat, invalid as per domain rules."
                    {:type :invalid-input
                     :input cat
                     :explain explanation})))))

(defn make-cat
  [name color hungry?]
  (assert-cat
   {:name name
    :color color
    :hungry? hungry?}))

(defn update-cat
  [cat & {:keys [name color hungry?] :as new-key-vals}]
  (assert-cat (merge cat new-key-vals)))
=> (make-cat "Kitty" :red true)
:red - failed: #{:white :blue :grey :black} in: [:color] at: [:color] spec: :
clojure.lang.ExceptionInfo: Can't make the requested cat, invalid as per domain rules.

=> (make-cat "Kitty" :grey false)
{:name "Kitty", :color :grey, :hungry? false}

=> (let [cat (make-cat "Kitty" :grey false)]
=>   (update-cat cat :hungry? true))
{:name "Kitty", :color :grey, :hungry? true}

=> (let [cat (make-cat "Kitty" :grey false)]
=>   (update-cat cat :hungry? :yes))
:yes - failed: boolean? in: [:hungry?] at: [:hungry?] spec: :?
clojure.lang.ExceptionInfo: Can't make the requested cat, invalid as per domain rules.

jjttjj16:02:34

I just realized this and it seems to fit here and maybe bring things full circle, but spec itself uses with-gen so there's an "officially sanctioned" use of with-* as a non destructive "setter" for you

didibus19:02:36

I feel with-meta and with-gen are about attaching some sort of context, not really modifying the value.

emccue20:02:46

very dumb q - does clojure have any equivalent to a package-info.java? I want to explain what the entire path of namespaces is for

dpsutton20:02:28

Clojure doesn't have a notion of packages. Namespaces are not related to each other. However, you can put doc strings on a namespace. For instance (doc clojure.test) to get a wall of information

2
respatialized20:02:31

Could you put a textual description into the classpath by creating a resources/my/lib/docs/namespaces.md or something? I don't know how discoverable that would be for users/consumers of your code, though

emccue20:02:21

its internal users, so an .md might be the thing

Alex Miller (Clojure team)20:02:02

You can put anything you like on the classpath and read it as a resource (but that’s pretty non-obvious unless you have tooling around it)

emccue20:02:58

yeah...I think empty namespace with doc vomit might be the way

respatialized20:02:51

💡 this just gave me an idea: a docs namespace for a project that augments namespace based docs with examples, longer descriptions, and even potentially other resources that could be used to create both reference material and interactive documentation

respatialized20:02:06

"docs as data"

dpsutton20:02:51

i'm a huge fan of querying the runtime but that sounds like putting documentation or a wiki inside of a jar which sounds like a poor fit.

Alex Miller (Clojure team)20:02:04

Isn’t that what the Clojure doc site is doing already?

respatialized20:02:17

something colocated with the project gives editor tooling more immediate access to those things than an external site

Alex Miller (Clojure team)20:02:46

It’s generating from the stuff in the project, so I think it’s both

respatialized20:02:41

I guess I'm again stealing inspiration from Unison, where docs, including examples, are first-class language components https://www.unison-lang.org/whats-new/latex-doc-support/

Alex Miller (Clojure team)20:02:49

But there is a cooler answer here, which would to establish a separate Maven classifier for a jar of docs that sat alongside the lib (which is how Java source and javadocs works)

2
Alex Miller (Clojure team)21:02:57

With a bit of thought for standardizing the metadata, this could be very amenable to Clojure tooling

2
Alex Miller (Clojure team)21:02:16

(And would be a very cool Clojurists Together project imo)

👍 2
respatialized21:02:00

I think that's the right approach overall, I think being able to separate the necessary code from dev-time examples is an important consideration for production deployments

jpmonettas11:02:26

I'm doing something like that https://github.com/jpmonettas/flow-storm-debugger/blob/master/docs/flow_docs.md The idea for flow-docs is that it creates a jar containing a flow-docs.edn file which contains information on functions parameters and return values types together with call examples, and more. So you can then add the docs dependendencies to your dev classpath and a tool (in this case FlowStorm) will grab all those docs edn files in the classpath and render documentation in a GUI