Abstract Clojuretions for Christoph - where to put custom fields? how to call it? - bag of entitites - create some custom ones, or how do you go about it? attach an example of the airbank bag of entities - Listening to [Functional Design in Clojure](https://clojuredesign.club/) makes me feel like I'm in a bar with two senior dev peers chatting about stuff that somehow all interests us, and I just want to soak as much their knowledge as possible. ## Unsorted / Misc Some things I have picked up so far (when I remembered, I added episodes numbers): 2. **Naming:** althought not stated explicitly, think of components (and maybe functions as well?) like persons, such as decider, reducer, mapper, handler, transformer, worker, handler 1. API clients: 1. consider having one fn for all calls, such as cognitects aws-api (104, 026) 1. **REPL:** - you can use REPL outside of app development, say to explore APIs, dbs. (099, 104) - next to an app code tree, have a dev tree with fiddles, it's like your workbench where you explore stuff and once done, move them to the app tree. One can use other tools, but eventually you need to use code, so you are getting pieces in and learning about libs, you can fiddle/explore abstractions as well (014, 102) - fiddles are like persistant and documented history that one can go back to, a tool for random-access exploration; fits well with “sw eng is a learning process, working code is a side-effect” - nil: - Most clojure core operates on nil, feel free to write nil-tolerant code (047) open questions: - how to name components and functions? any driving principles? - single fn api client - is this generally applicable? ## order of params (episode 018) - For predicates, put bag of data first, so we can partial over the arg, example ... # Maps ### maps - flat vs nested *(from Functional Design in Clojure - episodes 045, 051)* - for known entities, default to flat maps with rich keys over nested maps, treat maps as bags of (entity) dimensions - nested maps are decontextualized, you have to know what referred it (id, name vs team/id, team/name); rich keys add context - feel free to denormalize and grow the maps wider and wider, add as much derived fields as useful (vs fns of dervied fields come with computation penalty), value-oriented programming - but, there are counter-opinions, for example: [Clojure Coding Guide](https://grishaev.me/en/clojure-guide/#keywords) ```clojure {:airb/datum-splatnosti "2023/44/d" :airb/castka-v-mene-uctu "34,52" :airb/amount 34.52} ``` ### maps - bag of entities (Christoph's [reply in Slack](https://clojurians.slack.com/archives/CKKPVDX53/p1706055515412909?thread_ts=1699637695.133179&cid=CKKPVDX53), ep 110) For bag of entities, use nested maps, so that we can pull out what we want. ```clojure {:request {...} :resposne {...} :account {...}} ``` There is the instrumentation function, and there is individual step functions. - Step functions might need access to the whole bag (!), so give them the whole bag. - Step functions might be adding various details, leave it up to those functions to add those. - The instrumentation function only needs to know the basic shape of input and output, say, is input a map, or a seq of maps? Does the instrumentation function adds entities, or the indivi? Each step adds a new entity to the bag. - - Give those steps access to the whole bag (!), it will be super easy to propagate/add any other information - Extractors might need access to the whole bag (!), that's the whole point, to make it easy; so ```clojure ; before (defn read-raw-airbank-tx! [dir] (->> dir (fs-utils/find-csvs!) (mapcat read-csv!) (map to-raw-tx))) ``` > -The only way to know what the program was doing, is to know what the program was doing. If you're trying to figure out what the program was doing by reverse engineeringing it, you're going to get it wrong. (Ep110) ### maps - encoding enums *(from my experience)* Default to encapsulating an enum into one key: ```clojure {:direction :incoming} ``` Second approach is to denormalize an enum value into individual predicate keys ```clojure {:incoming? true :outgoing? false} ``` Advantage is the second approach is easier querying, especially with destructuring. However, the down-sides are: - destructuring might get bloated, but one needs to handle naming conflicts (if any) - map requires a change after a new enum option - there's a bit of design tension how to handle naming of the key, especially when the enum value itself is needed | | approach 1: <br>enum in one key | approach 2: <br>denormalized | | ---- | ---- | ---- | | destructuring | | might get bloated, name collisions | | new vals | | requires a change | ### maps - enriching - ?? not sure about this ?? *(from my experience, part from 018)* Extractors (providing view of data) could be reused by different bags of data, as long as they contain the required shape. Separate computing the value with assoc to a map: ```clojure ; extractors (defn direction [tx] ...) (defn card-holder [tx] ...) ; enrichers (defn enrich [tx] (assoc tx :direction (direction tx) :card-holder (card-holder tx))) ``` The second approach is to have a set of enrichers: ```clojure (defn with-direction [tx] (let [direction ...] (assoc tx :direction direction))) (defn with-card-holder [tx] (let [card-holder ...] (assoc tx :card-holder card-holder))) (defn enrich [tx] (-> tx with-direction with-card-holder)) ``` Discussion: - mixing concerns: the second approach mixes computation of the value with expanding the map; by decomplecting (the first approach), one gets more options how to deal with the computed values - testing: easier with first approach, testing less things at once - locality (things that change together should be located together): enriching the map at one place gives high-level overview of enrichment and naming - code: first approach leads to smaller functions with single responsibility, one might discover further mixing of concerns inside that value fn, especially if it returns multiple vals at once - can we flatten it? - for dependencies - see the next item ## maps - keys - raw data to domain data (from ep108) Outside world is messy and we don’t have control over it. We need an explicit model that we use to *reason about*. Keep working data light, could be checked with spec. Put raw data into `raw` object. Any domain model is an extractor from a raw object For example, raw for transactions, tx for model transactions. Avoid overcomplication by prematurely generating more domain types in the same map, if these are not immediately visible. ## Maps - representing an entity (Episode 031) Use :kind key to determine type of entity, and then prefix keys with that entity. For example: :kind sprinkle, :sprinkle/user-id ### maps - enriching with dependencies (? obsolete - need to figure this out ) *(from my experience)* - How to deal with ([similar problem with components](https://youtu.be/13cmHf_kt-Q?si=TDKOXh6E5uD6iV_k&t=376), but now in the context of enrichers): - hidden dependencies - manual ordering If enriching in the previous item relies on dependencies, use threading for assoc? ```clojure ; example of a value that depends on previously computed values (defn category [{:extra/keys [direction card-holder]}] ... ) (defn enrich [tx] (-> tx (assoc :extra/direction (direction tx) (assoc :extra/card-holder (card-holder tx)))) (assoc :extra/category (category tx)) ``` Other approaches: 2. in the `category` fn, call previously computed values again 3. in `enrich`, use a let block Threading assoc's seems to be the best option: single-responsibility, flat structure relying on clojure core, no recomputations, no duplicated logic. ## maps - replace with records only for truly polymorphic behavior [Thinking in Data - Stuart Sierra - YouTube](https://www.youtube.com/watch?v=kuNxVXnmjHA) ## IO workers are aggresively thin - For calling rest api, io, etc, the handlers are trivial; - all prep and post work is done in pure fns # Core functions - The three common functions that create functions: partial, juxt, apply ## partial *(Ep 078, and some other episodes about maps) - At the end of a threading sequence, say with `juxt` and `partial apply` - Especially for exploring data - Predicates to be used with filter ## juxt (Ep 76) - It is like select-vals, but more regenrsl ## apply - Typically at the bottom of the threading forms - With var-args, comes apply - Some var-args fn dont work with empty args - you can guard against this with thread-some macro - Or supply an arg, eg `apply max 0` - Idiomatic usage 1. `apply max`, `apply str` 2. `apply concat` - infamous flatten but only on one level - `apply concat map` is `mapcat` # Components ### Structuring things ??? - make every function only depend on things passed to it in its arguments - no fn reaches out to global def - things with static config (DB connections, API keys, URLs) - external stateful resources with run state (db connection, socket connection, file handle, session, oauth2 token) - process state (threads, refs/atoms, core async) Dont: - putting resources into a ring request map complects the pure request with io stuff, instead put resources as fn params (episode 067) Resources - [Stuart Sierra - Components Just Enough Structure - YouTube](https://www.youtube.com/watch?v=13cmHf_kt-Q) - [usermanager-example/src/usermanager/main.clj at develop · seancorfield/usermanager-example · GitHub](https://github.com/seancorfield/usermanager-example/blob/develop/src/usermanager/main.clj#L202) - [GitHub - juxt/clip: Light structure and support for dependency injection](https://github.com/juxt/clip?tab=readme-ov-file#comparison-with-other-libraries) - [(iterate think thoughts): Contrasting Component and Mount](https://yogthos.net/posts/2016-01-19-ContrastingComponentAndMount.html) - episode 097,, episode 067: most popular ones are Component and Integrant - There's Component, Integrant, Mount and Clip - Most Metosin devs use Integrant [Clojure Integrant Exercise | Kari Marttila Blog](https://www.karimarttila.fi/clojure/2020/09/07/clojure-integrant-exercise.html) - [Site Unreachable](https://grishaev.me/en/clj-book-systems/#summary) - seems he preferrs integrant as well - kit uses integrant as well: [Kit Framework](https://kit-clj.github.io/docs/integrant.html) - episode 067: most popular ones are Component and Integrant - Most Metosin devs use Integrant [Clojure Integrant Exercise | Kari Marttila Blog](https://www.karimarttila.fi/clojure/2020/09/07/clojure-integrant-exercise.html) - [Site Unreachable](https://grishaev.me/en/clj-book-systems/#summary) - seems he preferrs integrant as well - kit uses integrant as well: [Kit Framework](https://kit-clj.github.io/docs/integrant.html) - [Site Unreachable](https://mccue.dev/pages/12-7-22-clojure-web-primer) - integrant: recommended by JUXT clojure radar - [Site Unreachable](https://www.juxt.pro/blog/abstract-clojure/) Approaches: 1. use global singletons and global effects 2. each fn take all deps as params (component) ## Accessing db from handlers I don't know how to structure code so that the handlers have access to the repos. Some approaches: 1. Make repo a Protocol, have each component implement such a protocol 2. Make them a function, pass functions to handlers ([Abstract Clojure](https://www.juxt.pro/blog/abstract-clojure/#)) ```clojure ; approach 1 (defn handle-register [create-user send-user-info id pw] (create-user id pw) (send-user-info id pw)) ; approach 2 (defn handle-register [db id pw] (db/create-user db id pw) (user/send-user-info id pw)) ; On external level, (handle-register create-user send-user-info id pw) (handle-register db id pw) ``` - understanding the function: `db/create-user` has documentation and signature which I can easily check; create-user as an argument would require some specs - `db/create-user ### Creating fakers - [comment by weavejester](https://github.com/weavejester/integrant/issues/81#issuecomment-615906828) - on how this is commonly done - - # Composition - organize code such that you don't need to mock; - put side-effects to the edges, call side-effects as high-up the stack trace as possible; take this principle as see where it guides you when applied rigorously (026 end) - keep decomposing side-effect functions from pure-functions (093-098). For example, separate calling an API into constructing the pure fully-deterministic request (pure fn with operation instruction) and calling the actual side-effect (execution) (104) - **Fakers:** use fake resources, such as fake twitter api, to speed up development. Fakers are not testing, it's for deving. Fake inside the app, say, have a second faker component next to the production component (043, 025) - ??? where to put the fakers? [Thinking in Data - Stuart Sierra - YouTube](https://youtu.be/kuNxVXnmjHA?si=9257OEg1T04PgzsV&t=1762) # Frontend ## naming things - "component", "page" - Andrew uses "component", biff uses "component" - [htmx | Biff](https://biffweb.com/docs/reference/htmx/) -