clojuredesign-podcast

neumann 2024-01-04T18:14:52.536819Z

How do you grow your codebase from the bottom up? Why does that prevent a lot of pain and hassle? How does Clojure help you do that? In our latest episode, we grow beyond our REPL-driven pieces toward an end-to-end solution. https://clojuredesign.club/episode/105-codify/

πŸ‘ 2
nate 2024-01-05T19:30:40.422709Z

> I'm not smart enough to devise a grand plan without hands-on experience. @leif.eric.fredheim I'm thoroughly convinced that no one is "smart enough". Those that think they are often build a beautifully incorrect solution. The best way to learn about a new domain is to mess around with it in the connected editor. Like you said, you'll get lots of small wins and stay motivated. You will also be working with the real dependencies and learning how they behave and fail. After enough time, you'll have learned enough to come up with a design that will work for the problem.

πŸ‘ 1
JR 2024-01-06T17:26:17.091569Z

Beautifully incorrect - I sometimes feel that way when I look back at my old code. πŸ™‚ I liked the idea of using a map to hold intermediate values. I hadn't thought to do that. It's a useful idea. Also, I had to come here to say that I was associated with a turtle-themed team. As you surmised, no highlight reels were created. 🐒

πŸ˜† 1
🐒 1
neumann 2024-01-04T18:15:04.688519Z

Are you a fan of bottom-up development? Tell us why.

leifericf 2024-01-04T19:39:54.818159Z

Yes! Because I'm not smart enough to devise a grand plan without hands-on experience. I need to try things and see what happens, learn, try again, etc. I prefer a bottom-up and exploratory approach because it helps me gradually work towards my goal and learn incrementally at the same time. There are many small wins along the way, so it keeps things interesting and prevents me from feeling lost or demotivated. You also get a little dopamine rush every time you evaluate an expression in the REPL, especially if it works on the first try.

2024-01-05T01:56:12.324809Z

I'd like to see the highlights for the Dancing Hippos vs the Rampaging Sloths, New Year's Day

πŸ˜† 2
neumann 2024-01-05T03:17:43.185299Z

@leif.eric.fredheim Funny you mention it, because I was just chatting with @nate about why I think a REPL-based workflow feels so satisfying. It's definitely a dopamine rush! Like you're saying, it's a steady stream of little wins! And yes, on the practical side, bottom-up keeps you grounded in the way things are.

leifericf 2024-01-04T19:30:42.932939Z

Great episode (105), guys! It dropped just as my son fell asleep, and I got to enjoy it while doing the dishes after dinner. Now I'm seated in front of my laptop with a cold beer and about to finalize the end-of-year bookkeeping and government reporting for my wife's companies. But I want to share a thought first! You mentioned using let blocks a few times in this episode. This caught my ear because in the Norwegian Clojure sub-community, we have sort of converged on refraining from using let blocks within functions unless they define something that will be used many times or to give something a meaningful name. The general consensus seems to be that it is better to split things up into discreet functions that are threadable and use threading macros to define the "recipes." In this way, the code can be read only one time from top to bottom to be understood, i.e., using https://en.wikipedia.org/wiki/Tacit_programming. @christian767 might be able to add some more nuance or correct me if I misunderstood something. Perhaps you were referring to using a "free-standing" let block (outside a function) to experiment in the REPL before eventually incorporating it into a function (in an upcoming episode). I'm not sure if I fully understood what you meant. Any thoughts on that? Cheers! And I'm looking forward to the next episode already.

nate 2024-01-05T19:18:04.282699Z

@leif.eric.fredheim I understand where you are coming from. Using let bindings for single usages is something I normally shy away from. In this case, one reason to prefer let bindings is the second reason you gave (give something a meaningful name). This helps with understanding the data as development progresses. It also makes it easy to tap out one or more of the bindings for inspection. To make this a bit more concrete (using concepts from the series), here's some example code:

(defn query-database
  [_team-names _game-date]
  [,,,])

(defn fetch-clips-from-mam
  [_clip-tags]
  [,,,])

(defn fetch-videos-from-s3
  [_clips]
  [,,,])

(defn concatenate-clips
  [_files]
  ,,,)
If these were properly coded, we could do this:
(defn assemble-highlight
  [team-names game-date]
  (-> (query-database team-names game-date)
      fetch-clips-from-mam
      fetch-videos-from-s3
      concatenate-clips))
Or, with let bindings:
(defn assemble-highlight
  [team-names game-date]
  (let [clip-tags (query-database team-names game-date)
        clip-infos (fetch-clips-from-mam clip-tags)
        clip-files (fetch-videos-from-s3 clip-infos)
        final-highlight-file (concatenate-clips clip-files)]
    final-highlight-file))
That already gets us more inspectibility and understanding of the intermediate data.

πŸ’‘ 1
nate 2024-01-05T19:21:06.979379Z

Looking at your two functions, I have the same reaction that Jason did, the fetch-all function looks good and I would introduce the let in fetch for understandibility. I was tripped up with this:

(cond-> cont-key
        (assoc-in [:query-params (get-in request [:page :key-fn])] cont-key))
thinking that the cont-key was the thing being threaded when in reality it was the condition and the request is what's being threaded.

πŸ’‘ 1
nate 2024-01-05T19:21:25.079929Z

Kudos for using iteration. I've used it a few times and I think it's a great addition to the language.

πŸ‘ 1
leifericf 2024-01-05T19:39:55.904439Z

Aha, yes, I see what you mean about cond-> I felt like that was an elegant way of adding the continuation token to the request if it exists. But I get how that can be confusing now.

leifericf 2024-01-05T19:40:28.620649Z

And I'm embarrassed how long it took to understand how iteration works, haha πŸ˜‚ That was a doozie of a higher order function.

nate 2024-01-05T19:40:55.563769Z

hahaha, me too, it took lots of tapping and iterating to get it right

nate 2024-01-05T19:41:03.000519Z

but once I did, it's a thing of magic

nate 2024-01-05T19:42:01.949239Z

Referring to the cond->, another idea is to format it slightly different:

(cond->
        cont-key (assoc-in [:query-params (get-in request [:page :key-fn])] cont-key))

leifericf 2024-01-05T19:42:05.074519Z

Yeah! That happens a lot: I look at the docs and think, "what in the everloving hell is going on here?!" and then I get lost in the valley of dispair for a few hours or days… And then suddenly it clicks. And after that I think, "hey, that's actually quite easy!"

nate 2024-01-05T19:43:01.424709Z

too true

leifericf 2024-01-05T19:43:37.766229Z

The key for me to get iteration was to remember that keywords are also functions. I was writing these complicated anonymous functions for iteration, when I simply needed to pass it a keyword.

nate 2024-01-05T19:44:32.169459Z

yeah, that design decision (making keywords work as functions) has been so useful in Clojure

πŸ‘ 1
leifericf 2024-01-05T19:46:54.007529Z

I suspect I'm also committing a sin here, by threading into an anonymous function to get the parameter in the right place πŸ˜… https://gist.github.com/leifericf/242fb222966c8cfc8beb04539aaf8fab#file-pulumi-clj-L99

nate 2024-01-05T19:48:16.216869Z

I think that one is fine. You are constructing a mapper function, not something at the top level of the pipeline

πŸ’‘ 1
genekim 2024-02-04T07:42:05.114049Z

@leif.eric.fredheim @jason.bullers @nate Thanks for the amazing fetch and fetch-all. I think this is some of the most beautiful code I’ve seen, because of how easy iteration becomes β€” I rewrote my Google Photos queries, and am frankly a little shocked at how easily iteration fell out of it. My API calls will never look the same! PS: I had some problems with getting a NPE deep within hato http library. The problem was I was using uri as a map entry, which I copied from @leif.eric.fredheim’s gist. I had to change it to url in order for it to work with hato and clj-http libraries. (Otherwise, (name scheme) threw exception. πŸ€·β€β™‚οΈ PPS: I couldn’t get a stack trace, so I had no idea what was going on. I’m finding that I’m adding -XX:-OmitStackTraceInFastThrow` to all my JVM opts now.

❀️ 2
genekim 2024-02-29T21:16:32.994679Z

@leif.eric.fredheim Your style of fetch/fetch-all is finding itself multiplying in my code. I’m using it everywhere. TY!

πŸ™‡ 1
❀️ 1
leifericf 2024-03-01T05:23:39.525309Z

You're welcome, and I'm glad it was helpful! It's incredible how much one can get done with so little code and maintain readability πŸ˜„

leifericf 2024-02-04T08:10:00.577459Z

@genekim, thank you for the kind words! I've been a fan of your writing for a long time. Long before I started learning Clojure. It means a lot to me that you found it useful. It feels like getting complimented by Yngwie Malmsteen on my comparatively mediocre guitar skills.

❀️ 1
πŸ™ 1
slipset 2024-02-04T10:09:20.965129Z

Just wanted to share a something. iterations is a variation of unfold, which is the opposite of a fold, which we in Clojure call reduce, which again is a generalization of recursion. From this we have that an unfold is really co-recursion. As it were, that’s also the name of the podcast (Corecursive) which taught me most of this. https://corecursive.com/046-don-and-adam-folds/ is the episode where it all unfolds.

πŸ‘ 1
genekim 2024-02-04T17:48:48.857339Z

I loved that episode!!

πŸ’œ 1
genekim 2024-02-04T17:48:56.991339Z

I loved that episode!!

leifericf 2024-02-04T19:07:32.869819Z

Me too! It helped me understand how iteration worked, along with help from my good friends in #clojure-norway, who also helped me arrive at the code in my gist. We have several long threads about it somewhere in that channel (but they're in Norwegian).

2024-01-04T20:16:48.101829Z

I'm not sure if it's in that episode, but I recall some discussion around keeping the form that returns the function result as small and readable as possible. This means using let to build up all the pieces, including intermediate calculation results. Personally, I like that idea a bit better than just a single threading macro because it gives a place for naming and destructuring. Granted, I haven't built anything significant with Clojure, but in some coding puzzles I've done, I've found that nice little threading macro pipelines were fun to write, but difficult to understand a few weeks later.

jumar 2024-01-04T20:27:06.214499Z

This might be relevant https://stuartsierra.com/2018/07/06/threading-with-style

leifericf 2024-01-04T21:05:14.852799Z

Thanks for the input, guys! I'll check out those links. I'm curious to hear what you think about https://gist.github.com/leifericf/242fb222966c8cfc8beb04539aaf8fab#file-pulumi-clj-L7-L24 I recently wrote. They make use of ->, and fetch even has a "nested" cond->. I find this style quite readable, but perhaps it would be even more readable with let blocks? Or is this an example where using threads makes sense?

2024-01-04T22:06:18.547779Z

I think fetch-all is pretty clear. fetch I think would be clearer by breaking up the request and response pieces, and the dynamic part of the nested path. Maybe something like this:

(defn fetch
  "Perform an HTTP request, injecting a continuation key if supplied."
  [request & [cont-key]]
  (let [key-fn (get-in request [:page :key-fn])
        request (cond-> (:request request)
                        cont-key (assoc-in [:query-params key-fn] cont-key))
        response (http/request request)]
     (json/read-str (:body response) keyword))
I suppose you could do similar with functions too, and have one that's just responsible for building up the request. That would also streamline the fetch function a bit.

πŸ‘ 1
Bob B 2024-01-04T22:25:20.540189Z

the other thing I was going to suggest was destructuring the input params to its keys, largely because IMO it feels like a gap in naming to call (:request request); which I read as "get the request from the request"

πŸ‘ 1
πŸ‘πŸ» 1