This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-02-06
Channels
- # announcements (58)
- # babashka (43)
- # babashka-sci-dev (22)
- # beginners (8)
- # biff (8)
- # calva (62)
- # circleci (3)
- # clerk (6)
- # clj-kondo (27)
- # cljsrn (9)
- # clojure (61)
- # clojure-austin (4)
- # clojure-conj (3)
- # clojure-europe (11)
- # clojure-losangeles (2)
- # clojure-nl (2)
- # clojure-norway (4)
- # clojure-spain (5)
- # clojure-uk (2)
- # clojurescript (51)
- # data-science (1)
- # datascript (4)
- # emacs (33)
- # events (14)
- # funcool (14)
- # gratitude (13)
- # introduce-yourself (1)
- # jobs (9)
- # lsp (58)
- # malli (23)
- # missionary (31)
- # nextjournal (9)
- # off-topic (35)
- # proletarian (2)
- # re-frame (5)
- # remote-jobs (7)
- # shadow-cljs (2)
- # spacemacs (7)
- # sql (26)
- # testing (12)
- # vim (1)
- # web-security (3)
- # xtdb (2)
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.
with-
is more often used as a macro that binds around a body
with-meta
is an exception there
is there some reason you need this function at all?
vs just assoc etc
I try to avoid writing things that are just "getter" / "setter" style fns and lean on clojure core for those
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
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.
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
. 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
.
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)
> 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.
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
assign-*
is interesting. Not clear if if there’s a principled argument for it over set-*
or add-*
.
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"
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.
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.
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.
Your "CPU map" seems like a situation where I'd prefer your approach, @U4986ECDQ.
In Clojure, I would expect all functions to be non-destructive to its input, unless it ended with a !
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
In your cat
ns, you should probably have fns which reflect what you do with a cat in the domain you’re modelling.
@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?
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.
Yeah, that’s intuitive to me. I bet the same principle applies for functions that manipulate cats.
e.g. If naming already-constructed cats is common, and you want to do some validation on names as they’re assigned.
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)
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
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
Using aliases auto referred keywords is a good way to use good kw names with less typing
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.
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.
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
I feel with-meta and with-gen are about attaching some sort of context, not really modifying the value.
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
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
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
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)
💡 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
"docs as data"
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.
Isn’t that what the Clojure doc site is doing already?
something colocated with the project gives editor tooling more immediate access to those things than an external site
It’s generating from the stuff in the project, so I think it’s both
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/
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)
With a bit of thought for standardizing the metadata, this could be very amenable to Clojure tooling
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
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