|
| 1 | +^{:kindly/hide-code true |
| 2 | + :clay {:title "Progressive — Hypertrophy with clojure(script)" |
| 3 | + :quarto {:author :ameinel |
| 4 | + :description "An introduction to progressive - a workout tracker" |
| 5 | + :type :post |
| 6 | + :draft true |
| 7 | + :date "2026-02-11" |
| 8 | + :category :fitness |
| 9 | + :tags [:fitness :sports :functional-programming :pwa]}}} |
| 10 | + |
| 11 | +(ns progressive.hypertrophy-introduction) |
| 12 | + |
| 13 | +;; ## What is Progressive? |
| 14 | +;; |
| 15 | +;; [Progressive](https://github.com/Schroedingberg/progressive) is a clojurescript app for tracking hypertrophy oriented workouts - some would also call it lifting weights, though technically that's not the core problem this solves. |
| 16 | +;; The motivation is simple: Decide for a plan, train according to it, log your performance, get your next workout calculated based on established models for hypertrophy ("building muscle"). Then repeat. |
| 17 | +;; |
| 18 | +;; ## Why build another fitness tracker? |
| 19 | +;; |
| 20 | +;; I wanted something that is really simple, works on my phone, doesn't need a server and is FOSS. Surprisingly, that's hard to find. There are a few workout trackers on F-Droid, but none of them do what I need them to do. I'll go into detail about that later. Apart from these requirements, I wanted to hone my clojure skills. |
| 21 | + |
| 22 | +;; ## Requirements |
| 23 | +;; |
| 24 | +;; 1. Data first: I wanted to get something that I can use as quickly as possible, while keeping it as simple as possible. Therefore, I needed a flat workout log - to collect the data. Some would call those pieces of data events. In fact, I tend to do that too, although with caution (it would be an overstatement to call this an event-sourced system). |
| 25 | +;; |
| 26 | +;; 2. Local first: I went through many design drafts until I arrived at the current design, which were all [server-side apps](https://github.com/Schroedingberg/romance-progression). While from a development perspective that is my comfort-zone, I soon arrived at the conclusion that for the problem I was trying to solve, the overhead of running a server was just not justifiable. So ultimately I decided to dive deep into the unknown realms of frontend development and build this as a pwa, using reagent. The core logic is agnostic of that, so I'll keep the frontend part for another post. |
| 27 | +;; |
| 28 | +;; 3. Flexible progression algorithms: This overlaps with 1. a bit, but it is important to mention this explicitly. There's a lot of research about how to progress through a block of workouts for ideal hypertrophy. At the same time, exercise science is surprisingly hard to do - if you look at studies you often find sample sizes that would disqualify the data in other research fields. Therefore you may well find that whatever awesome progression somebody comes up with, it may not work for you. Or maybe you just can't keep up with it and have to just train "good enough", so you don't really need your progressions calculated for you at all. Maybe you just want to log what you did and try to do a bit more, a bit heavier next time. In any case, I wanted to keep the progression calculation completely separate of the logging functionality. |
| 29 | + |
| 30 | + |
| 31 | +;; ## Show me code! |
| 32 | +;; |
| 33 | +;; We don't want to just walk into the gym and randomly lift weights. |
| 34 | +;; We need a plan! The largest unit that makes sense to me is a block of roughly four weeks, which the community calls a mesocycle. A mesocycle consists |
| 35 | +;; of microcycles. So a block of four weeks usually has four microcycles (you could use any period length actually, but let's stick with what most people are used to). In each microcycle you do a number of workouts and each workout has exercises, of which you do a number of sets. |
| 36 | +;; > NOTE: In this context, 'set' refers to a set of an exercise, which is the elementary unit of work I want to track. Each set represents a real world event like 'n repetitions at weight m for exercise X'. This might get confusing, because clojure also has a function 'set'. Whenever I am talking about sets in this context, I am referring to a set in the exercise sense, not in the mathematical or 'hash-set' sense. You have been warned! |
| 37 | + |
| 38 | +;; Sounds pretty hierarchical, huh? Luckily, hierarchical data structures are something we're all really comfortable with. |
| 39 | + |
| 40 | +;; A mesocycle might look like this (we show only the first week here for brevity): |
| 41 | + |
| 42 | +{"Just squat twice per week, for two weeks" ;; This is what we call the mesocycle |
| 43 | + {0 ;; microcycle index |
| 44 | + {:monday |
| 45 | + {"Squat" |
| 46 | + [{:exercise-name "Squat" |
| 47 | + :muscle-groups [:quads]} |
| 48 | + {:exercise-name "Squat" |
| 49 | + :muscle-groups [:quads]}]} |
| 50 | + :thursday |
| 51 | + {"Squat" |
| 52 | + [{:exercise-name "Squat" |
| 53 | + :muscle-groups [:quads]} |
| 54 | + {:exercise-name "Squat" |
| 55 | + :muscle-groups [:quads]}]}}}} |
| 56 | + |
| 57 | +;; Ok, that's roughly what we want the plan for a mesocycle to look like. |
| 58 | +;; Naturally, we can't be bothered to type the full plan - it's going to be the same every week, fundamentally. |
| 59 | +;; We need a kind of template, that lets us express what we want to do more concisely: |
| 60 | + |
| 61 | +(def template |
| 62 | + {:name "2x Minimal Full Body" |
| 63 | + :n-microcycles 4 |
| 64 | + :workouts |
| 65 | + {:monday |
| 66 | + {:exercises {"Dumbbell Row" {:n-sets 2 :muscle-groups [:back]} |
| 67 | + "Dumbbell Press (Incline)" {:n-sets 2 :muscle-groups [:chest :shoulders]} |
| 68 | + "Lying Dumbbell Curl" {:n-sets 3 :muscle-groups [:biceps]} |
| 69 | + "Back Raise" {:n-sets 1 :muscle-groups [:hamstrings]} |
| 70 | + "Reverse Lunge Dumbbell" {:n-sets 2 :muscle-groups [:glutes :quads]} |
| 71 | + "Sissy squat" {:n-sets 2 :muscle-groups [:quads]}}} |
| 72 | + :thursday |
| 73 | + {:exercises {"Back Raise" {:n-sets 1 :muscle-groups [:hamstrings]} |
| 74 | + "Barbell Squat" {:n-sets 2 :muscle-groups [:glutes :quads]} |
| 75 | + "Bench press (Narrow Grip)" {:n-sets 2 :muscle-groups [:chest :triceps]} |
| 76 | + "Pullup (Underhand Grip)" {:n-sets 2 :muscle-groups [:back :biceps]}}}}}) |
| 77 | + |
| 78 | +;; Neat! Not much more code, but we have a lot more exercises. This is my current plan by the way! |
| 79 | +;; Of course, to get a proper plan out of this, we need to expand this somehow: |
| 80 | + |
| 81 | + |
| 82 | +(defn expand-exercises |
| 83 | + "Expand {:n-sets 3 ...} into a vector of 3 set maps." |
| 84 | + [{:keys [exercises]}] |
| 85 | + (reduce-kv |
| 86 | + (fn [m name {:keys [n-sets] :as ex}] |
| 87 | + (assoc m name (vec (repeat n-sets (-> ex (dissoc :n-sets) (assoc :exercise-name name)))))) |
| 88 | + (array-map) |
| 89 | + exercises)) |
| 90 | + |
| 91 | +(defn ->plan |
| 92 | + "Expand a template into the full plan structure." |
| 93 | + [{:keys [name n-microcycles workouts]}] |
| 94 | + (let [expanded (update-vals workouts expand-exercises)] |
| 95 | + {name (into (sorted-map) |
| 96 | + (zipmap (range n-microcycles) |
| 97 | + (repeat n-microcycles expanded)))})) |
| 98 | +;; That will give us all the sets that we plan to do, as maps. |
| 99 | +;; You might ask yourself by now: Why would I want to put all that information into those maps that represent a set (like for instance the exercise name)? |
| 100 | +;; Glad you ask! Of course, this is not a random choice. I'll explain that next. |
| 101 | +;; TODO: Explain event structure |
0 commit comments