This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2022-03-29
Channels
- # announcements (7)
- # asami (13)
- # babashka (22)
- # beginners (52)
- # calva (95)
- # clj-kondo (14)
- # cljs-dev (7)
- # clojars (5)
- # clojure (94)
- # clojure-austin (5)
- # clojure-dev (15)
- # clojure-europe (25)
- # clojure-nl (18)
- # clojure-uk (15)
- # clojuredesign-podcast (28)
- # clojurescript (63)
- # copenhagen-clojurians (1)
- # cursive (3)
- # datalevin (7)
- # datascript (13)
- # datomic (13)
- # duct (14)
- # emacs (24)
- # events (1)
- # fulcro (13)
- # graphql (7)
- # kaocha (4)
- # lambdaisland (6)
- # lsp (22)
- # music (5)
- # off-topic (24)
- # rdf (1)
- # re-frame (3)
- # reitit (9)
- # shadow-cljs (23)
- # sql (15)
- # testing (4)
- # tools-build (6)
- # vim (7)
- # vscode (7)
- # xtdb (21)
I appreciate the idiom you guys (@neumann and @nate) talk about now and then: “Giving the computation a meaningful name.”
For example, instead of doing complicated logic in an if
statement within a let
block, extracting that logic to a separate predicate function. A related topic you guys have brought up a few times is how Clojure’s core functions provide a common language, making it easier to read code written by someone else without understanding their “custom functions.” Learning about all the core functions and how to use them in an idiomatic way requires much effort but allows programmers to collaborate more effectively.
Then I got to thinking about something… Isn’t there a subtle contradiction there?
On the one hand, one would want to use as many core functions as possible, so that the code is more readable to other experienced Clojure programmers. On the other hand, creating new “verbs” that “encapsulate” core function calls might be helpful to work at a higher level of abstraction and make the core more readable to an English speaker. When should we write code in such a way as to make it more readable “in Clojure” vs. “in English?” :thinking_face:
An experienced Clojure programmer might not have any problem reading a line of code where juxt
and map
are combined, i.e., when there are several “layers” of higher-order functions. As a novice Clojure programmer, I find such code a bit daunting, and I would probably find the code easier to read if that code was “hidden” behind a “custom verb” in plain English. But that’s more due to my inexperience than anything else.
When I read Clojure code written by others, I often encounter an expression and think to myself: “Wow, that’s extremely terse and elegant. But I have no idea what’s going on.” I can usually figure it out after spending a considerable amount of time staring at the code, playing around in the REPL, reading the documentation for core functions, etc. Writing more code and “custom functions” would make it more immediately readable to me as a novice.
Yeah, there's a tradeoff here which you need to consider based on the context.
E.g., I much preferred medley.core/map-vals
over the handwritten juxt-style mapping because it was shorter and much more readable even after I had seen the juxt-style idiom many times.
As a bonus, it was also more performant.
And with clojure 1.11 we have update-vals
so it's now a straightforward choice 🙂
I agree, i also have a hard time making sense of tense Clojure code and appreciate the introduction of meaningful let
symbols or function names…
I can't go back in time and be new to Clojure again, so I really appreciate you taking the time to write out your thoughts and providing that perspective! I have ingrained habits of thinking, so sharing how you see things helps me stretch my thinking out of my well-worn paths.
One of the ways I view things in my own head is the separation of the mechanics vs the intention. Complex expressions, even if they are built up with core and common idioms, don't implicitly communicate their intention. By putting that expression in a function and naming it, you can label the intention and purpose.
Definitely one approach is to add comments to express the intention, and I think comments can be very helpful, but this is where the "building up the vocabulary" part comes in. Those functions begin to create a space of expression for that problem space. Those functions turn into views of the data ("extractors"), important questions to be considered ("predicates") and define the operational space ("updates", "reducers", "transforms"). I tend to put all these functions together (co-located), and I can get a clear picture of what matters in that information model.
We get into specifics about this in Eps 15 - 19. In that, we develop a "computational space". Look here for the code: https://clojuredesign.club/episode/018-did-i-work-late-on-tuesday/
The information model is a sequence of maps which all contain :date
, :start
, :end
, and minutes
. From there we built up a "vocabulary". Consider this function:
(defn day-of-week
[entry]
(jt/day-of-week (-> entry :date)))
At first that function might seem a little silly. Why make a function for that at all? Why not just use jt/day-of-week (:data entry)
whenever you need to know? Turns out that vocabulary starts to compound.
(defn day-of-week?
[day entry]
(= (day-of-week entry) (jt/day-of-week day)))
Which otherwise would be:
(defn day-of-week?
[day entry]
(= (jt/day-of-week (-> entry :date)) (jt/day-of-week day)))
And then there is:
(defn weekend?
[entry]
(or (day-of-week? :saturday entry)
(day-of-week? :sunday entry)))
Which otherwise would be:
(defn weekend?
[entry]
(let [day (jt/day-of-week (-> entry :date))]
(or (= day (jt/day-of-week :saturday))
(= day (jt/day-of-week :sunday)))))
These are pretty trivial functions for a bit of a toy problem, but there is already an aspect of clarity. I won't belabor the point much more, but you can see that weekday?
could start to get ridiculous if we didn't depend on some other vocabulary:
(defn weekday?
[entry]
(not (weekend? entry)))
Also, as a new reader in the code, you now know that this problem space considers days of the week, weekends, and weekdays to be important. There are so many other dimensions of time that could be important, but in this code, apparently, they are not really all that important.
Again, this is pretty simple, but if I encounter total-minutes
in the code, that immediately tells me what it is, even though the equivalent is fairly trivial code:
(->> entries
(map :minutes)
(reduce +))
@U01PE7630AC I hope that helps explain my thinking a bit more. I'd love to show more complete code, but I don't have anything at hand that I can share. I'll just make due with what I've got for now.
Actually, I can give you a real world example. This is from an application I developed.
I wrote code that has to manage camera and microphone inputs for a web-based application. It has to deal with all sorts of edge cases. I created a determine-action
function that will return the "right" thing to do based on the current state of the manager component.
I might be wrong, but I'm guessing you go figure out all of the important edge cases even without much in the way of commenting. This is an example of working at the "problem space" level, not the "mechanics" level.
(defn determine-action
"Returns a tuple of the action to take and the reason for the action."
[data]
(let [{:keys [blocked? media-devices media-input waiting-for-media?]
:cache/keys [settings url-settings]} data]
; Order matters because the first match wins. Be careful not to move a
; condition earlier than its guard conditions.
(cond
(nil? settings) [:no-op :settings]
(nil? url-settings) [:no-op :url-settings]
waiting-for-media? [:no-op :waiting]
blocked? [:no-op :blocked]
(not (enabled? data)) (if (active-media? data)
[:stop-all-media :not-enabled]
[:no-op :not-enabled])
(nil? media-devices) [:detect-devices :no-devices]
(missing-input? data) [:no-op :missing-input]
(need-permission? data) [:ask-permission :need-permission]
(media-needed? data) [:start-inputs :need-media]
(device-removed? data) [:start-inputs :device-removed]
(not (selected-device-is-active? data)) [:start-inputs :selected-not-active]
; Camera settings can be applied on the fly without restarting the input.
(camera-settings-changed? data) [:apply-camera-settings :camera-settings-changed]
(settings-changed? data) [:start-inputs :settings-changed]
:else [:no-op :default])))
Of course, to understand how the state is represented, you have to look elsewhere. What constitutes a "missing input"? How do we know "media is needed"? Look at the functions. You can always come back here to see all the things that matter and how they are related to each other.
Those are some fantastic examples, @neumann! Thanks for taking the time to share those. Making "silly small functions" definitely makes sense, and I tend to do that in other languages as well for my day job. For example, here are two Python helper function I wrote today:
def extract_dict_key(dict, key):
return [d[key] for d in dict]
def transform_list_to_string(list, separator=' '):
return separator.join(map(str, list))
Prior to listening to your podcast, I would have just inlined those.@U01PE7630AC Neat! I'm not totally sure what the first one does, but in the second case, it's telling me that "this is the way the list is represented as a string". In Clojure, I tend to use "arrows" in my function. Eg. some-entity/->str
.
Yeah, the first one could definitely do with a better name. That was the best I could come up with in a hurry 😅 It basically returns a list of values which correspond to a particular key in a dictionary (kind of like a Clojure map… ish). I'm using it like this:
def get_google_account_names():
'''Get a list of all account names (unique ids).'''
google = get_google_api_interface(
get_google_credentials(),
service_name='mybusinessaccountmanagement',
service_version='v1',
service_discovery_url='')
accounts = google.accounts().list().execute()
return extract_dict_key(accounts['accounts'], 'name')
Which will return something like this:
['accounts/******************020',
'accounts/******************098',
'accounts/******************872',
'accounts/******************021',
'accounts/******************112']
Each of those entries is a value corresponding to the dictionary key name
, which is nested inside a dictionary key accounts
. Maybe something like get_vals_for_dict_key
would be better… But also not great 😅I think this post from Eric Normand is related to the discussion: https://ericnormand.me/issues/purelyfunctional-tv-newsletter-434-re-combination-of-parts I like how it discusses the tradeoffs, notably the argument that "optimizations" (whether it's performance or better design) could be achieved more easily if you don't hide everything behind an abstraction barrier: > But concision can make things easier to rework at a deeper level. > You are freed from the confines of the original domain modeling. And sometimes, you need that freedom to make a breakthrough
@neumann The advice you outline here is great. I'm going to very specifically try to add some flavor to it but realize this might not be the stew you needed in your case: Assuming the "jt" in
(defn weekend?
[entry]
(let [day (jt/day-of-week (-> entry :date))]
(or (= day (jt/day-of-week :saturday))
(= day (jt/day-of-week :sunday)))))
Is clj time https://github.com/clj-time/clj-time
Isn't there a weekend function already? (I'm honestly just pointing this out because i was surprised you needed to build one!)
https://github.com/clj-time/clj-time#clj-timepredicates
(require '[clj-time.core :as t])
(require '[clj-time.predicates :as pr])
(pr/weekday? (t/date-time 2014 1 26))
Now, what does this have to do with the larger message? Probably nothing, maybe your concept of weekend is different then the libraries?
FWIW though, this is what i would do
(def weekend-day #{:saturday :sunday})
;;mocked fn
(defn day-of-week [x] :saturday)
(some weekend-day (-> entry :date day-of-week)) #note not repl tested :(
The important change here is that the data about what is a weekend is now more accessible and extensible, maybe in some places in the world they have 3 day weekends (we can only hope).
As to the entire discussion, specifically
> Clojure’s core functions provide a common language, making it easier to read code written by someone else without understanding their “custom functions.”
The idea here is in the right direction, and again, i want to elaborate on it a bit and be a bit poetic at the same time. Because why not?
A function is beautiful because it is used according to it's nature. And what is it's nature? To provide a reference from a symbol to some logic. To sync those to things.
Thus the symbol that is only used once e.g
(defn foo [x] ...) # 1 reference
Isn't true to it's nature. It would be more true to the nature of the problem to just write a comment if you felt the functionalty needed commentary. I always see a set of fn names used in the place of trying to communicate something where a well written set of documentation over one block of code would have served much better.
I believe people conflate writing functions with building abstractions the same way that we chastise OOP for conflating Objects with abstractions. Neither tool is any more than what is . The advantage we have in clojure is that we have such a great core library of abstractions we all intuitively realize we should tap into it as much as possible.
The reason to be wary of creating custom functions is because it's very rare that an abstraction that you need won't already exist somewhere in a library. The reason to be wary of creating custom functions is because future readers will likely only be interested in reading your code at all because there expectations aren't being meet, which means they are going to drill pass your names tell they find ones they understand and can trust.
I personally don't care if people create a lot of helper functions, but I do care when these towers start to fall over and when you unwrap them you find that 80% of the code is aliases. That composition was thoughtlessly abandoned in favor a misguided attempt at "clean code" which the developer thought meant having lots of functions. I know why it happens, i have done it myself, but i'm trying to do better and sometimes that means having careful honest discussions and admitting to ourselves that we have grown in our understanding of the craft and business overtime. That we don't need to carry the sins of the past forward as it were 🙂 .@U0DJ4T5U1 It might be worth clarifying that @nate and I didn't miss the fact a "weekend" function already exists in the Java Time wrapper. The whole example is a bit of a toy problem. We were using it to try to provide concrete examples in a familiar problem domain. Since it's a podcast, we don't have the benefit visual illustration during the episode, so we wanted to pick something already understood. I suspect that many well-understood problems already have robust libraries available. Most of the time I spend programming in the "real world" is spent wrestling with a unique problem domain solved by the application (or set of libraries) I'm developing. I've found the concept of "building up language" to be extremely useful there. I hope the toy problem was useful enough to illustrate the concept without being too distracting in terms of "reinventing the wheel."
Thanks for the clarification.