A Survival Themed Probability Cheatsheet Zine
A printable PDF of an 8-panel foldable probability zine built with Clay.
A printable PDF of an 8-panel foldable probability zine built with Clay.
Reproducing a sociology simulation paper about social norms and the tendency towards low-quality exchanges and suboptimal outcomes.
The Unaccountability Machine1 by Dan Davies is broadly about how and why dysfunctional systems produce outcomes nobody seems to want. It also contains a tidy introduction to Cybernetics2 and ideas like the viable system model3 and requisite variety4.
Reading that got me interested in more systems and sociology topics, eventually leading me to a pair of papers:
Both were approachable and engaging despite my total unfamiliarity with game theory, sociology, and behavioral modeling. After 25 years in industry it was refreshing to see a formal take on corporate clique culture. And the papers were timely with a few things in my life:
So I committed to reproducing the paper’s findings in Clojure and its tables/figures (plus new ones) using Clerk.
This is a draft post. Clerk notebook and source code to follow.
Professionally, have you ever felt like your hard work wasn’t furthering your career, or even irritating some colleagues? Ever thought maybe things would be easier if you coasted a bit? Maybe you needn’t even feel bad about it if your peers had the same mindset.
The dissonance is reduced by interacting always with the same people, whom one can trust for not challenging one’s standards. L-doers segregate themselves in mutual admiration societies.
This is the mindset from which Gambetta & Origgi’s “cartels of mediocrity” arise. The LL game abstract:
We investigate a phenomenon which we have experienced as common when dealing with an assortment of Italian public and private institutions: people promise to exchange high quality goods and services (H), but then something goes wrong and the quality delivered is lower than promised (L). While this is perceived as ‘cheating’ by outsiders, insiders seem not only to adapt but to rely on this outcome. They do not resent low quality exchanges, in fact they seem to resent high quality ones, and are inclined to ostracise and avoid dealing with agents who deliver high quality. This equilibrium violates the standard preference ranking associated to the prisoner’s dilemma and similar games, whereby self-interested rational agents prefer to dish out low quality in exchange for high quality. While equally ‘lazy’, agents in our L-worlds are nonetheless oddly ‘pro-social’: to the advantage of maximizing their raw self-interest, they prefer to receive low quality provided that they too can in exchange deliver low quality without embarrassment. They develop a set of oblique social norms to sustain their preferred equilibrium when threatened by intrusions of high quality. We argue that cooperation is not always for the better: high quality collective outcomes are not only endangered by self-interested individual defectors, but by ‘cartels’ of mutually satisfied mediocrities.
And later in the LL game:
Our basic point so far can be summed up thus: if you give me L but in return you tolerate my L we collude on L-ness, we become friends in L-ness, just like friends we tolerate each other’s weaknesses. But if you give me H that leaves you free to disclose my L-ness and complain about it. So you are not my friend, I fear and resent you, and if I cannot punish you for producing H, at least I avoid dealing with you. While in an ordinary world it is L-doers who are punished by avoidance and exclusion, in an L-dominated world it is H-doers who are ostracised. Essentially, the L-exchange can be seen as a cartel of mediocrities who pretend to be H.
There seems to be two forces that could contrast our supposed natural inclinations to L-ness and promote quality, one is the passion for a job well-done, the intrinsic pleasure found in employing and testing one’s skills at some task; the other is competition, succeeding at which carries extrinsic rewards. Generically, these forces fail if the algebraic sum of rewards and punishments for H-ness is lower than the sum of rewards and punishments for L-ness. Even H-prone individuals are ultimately driven to choose L (or to become eccentric and isolated ‘perfectionists’ or to migrate) if they systematically fail to gain any reward from their effort. In short, L spreads if it pays off. This remains a tautology, however, unless we can understand the conditions that affect the relative payoffs of H and L.
I recommend reading the LL game just for the examples of different H/L exchanges, their psychosocial underpinnings, and the colorful Italian anecdotes8.
Social Norms and Low-Doers takes these ideas further with social agent simulation in NetLogo9. After modeling the social corrosion of L-worlds, it tests different regimes of rewards/sanctions to foster high-quality exchange:
Social norms play a fundamental role in holding groups together. The rationale behind most of them is to coordinate individual actions into a beneficial societal outcome. However, there are cases where pro-social behavior within a community seems, to the contrary, to cause inefficiencies and suboptimal collective outcomes. An explanation for this is that individuals in a society are of different types and their type determines the norm of fairness they adopt. Not all such norms are bound to be beneficial at the societal level. When individuals of different types meet a clash of norms can arise. This, in turn, can determine an advantage for the “wrong” type. We show this by a game-theoretic analysis in a very simple setting. To test this result – as well as its possible remedies – we also devise a specific simulation model. Our model is written in NETLOGO and is a first attempt to study our problem within an artificial environment that simulates the evolution of a society over time.
A society of agents that collaborate with each other, accumulate payoffs, age, retire, and get replaced by new hires over time. Their collaborations are simply a mutual exchange of abstract “goods”:
Assume for simplicity that goods can be produced at two levels of quality, High (H) and Low (L). H is both more rewarding to receive and more costly to produce than L; H takes more time, effort, skills and organisation.
Agents independently choose how much effort to put in: H (high) or L (low). Neither knows the other’s choice in advance. The four possible outcomes from any agent’s perspective:
| You | Them | |
|---|---|---|
| H | H | Quality work from both |
| H | L | You submitted quality work, they coasted |
| L | H | You coasted, they submitted quality work |
| L | L | Lazy slop from both |
Every agent has a type, which describes their ranking of preferred outcomes, e.g. HH > HL > LH > LL. All orderings of those four outcomes yield 24 possible types in all, each classifiable along two axes: a type is selfish when free-riding (LH) is its top choice, and high-minded when it ranks mutual-high (HH) above mutual-low (LL).
| # | Preference (worst < best) | Selfishness | Mindedness | Name |
|---|---|---|---|---|
| 1 | HL < LL < HH < LH | selfish | high | hs1 |
| 2 | LL < HL < HH < LH | selfish | high | hs2 |
| 3 | HL < HH < LL < LH | selfish | low | ls1 |
| 4 | HH < HL < LL < LH | selfish | low | ls2 |
| 5 | LL < HH < HL < LH | selfish | high | hs3 |
| 6 | HH < LL < HL < LH | selfish | low | ls3 |
| 7 | LL < LH < HL < HH | non-selfish | high | hn1 |
| 8 | LH < LL < HL < HH | non-selfish | high | hn2 |
| 9 | LL < HL < LH < HH | non-selfish | high | hn3 |
| 10 | HL < LL < LH < HH | non-selfish | high | hn4 |
| 11 | LH < HL < LL < HH | non-selfish | high | hn5 |
| 12 | HL < LH < LL < HH | non-selfish | high | hn6 |
| 13 | LH < HL < HH < LL | non-selfish | low | ln1 |
| 14 | HL < LH < HH < LL | non-selfish | low | ln2 |
| 15 | LH < HH < HL < LL | non-selfish | low | ln3 |
| 16 | HH < LH < HL < LL | non-selfish | low | ln4 |
| 17 | HL < HH < LH < LL | non-selfish | low | ln5 |
| 18 | HH < HL < LH < LL | non-selfish | low | ln6 |
| 19 | LH < HH < LL < HL | non-selfish | low | ln7 |
| 20 | HH < LH < LL < HL | non-selfish | low | ln8 |
| 21 | LH < LL < HH < HL | non-selfish | high | hn7 |
| 22 | LL < LH < HH < HL | non-selfish | high | hn8 |
| 23 | HH < LL < LH < HL | non-selfish | low | ln9 |
| 24 | LL < HH < LH < HL | non-selfish | high | hn9 |
Proietti & Franco focus on two canonical types to start. Both are selfish (their top preference is giving L and receiving H) but they differ in their mindedness (which they’d prefer if selfishness isn’t on the table.)
Such agents are arguably likely to be found in a competitive society where individuals are incentivized to participate in many activities (for example improving their CV by publishing, teaching, participating to conferences and research projects) while at the same time economizing their efforts and getting the most out of them.
Payoffs follow the preference ordering:
Agents can reconsider their strategy after an exchange. Each agent tracks two running tallies per partner (which are reset whenever the agent changes strategy):
When the shortfall crosses a threshold, a negative balance triggers a switch in the agent’s baseline strategy.
The asymmetry for the hs1 vs ls1 collaborations is in their opening play: hs1 starts at H and can fall short, but ls1 starts at L and in an LL exchange already earns its second-best outcome.
sequenceDiagram
participant hs1
participant ls1
Note over hs1: plays H (baseline)
Note over ls1: plays L (baseline)
hs1->>ls1: H
ls1->>hs1: L
Note over hs1: earns 1 (sucker)
shortfall +2, balance −1
Note over ls1: earns 4 (free-rider)
Note over hs1: shortfall ≥ threshold
balance < 0 → switches to L
loop Every subsequent round
hs1->>ls1: L
ls1->>hs1: L
Note over hs1: earns 2, shortfall grows
but switching back to H
would only earn 1 --- stuck
Note over ls1: earns 3 (preferred LL)
end
Proietti & Franco implement the model simulation in NetLogo, and I based my reproduction on their published source code10.
Each simulated year:
The institution hires 50/50 from hs1/ls1. The mechanism that tips the balance is in step 6: ls1 agents systematically out-earn hs1 agents over time, so hs1 agents have shorter careers/earlier retirements. Try-hards burn out and low-doers inherit the org: fresh hs1 replacements (hired 50/50) start out playing H, get exploited again, and the cycle repeats.
The agent state is a plain map; the memory map is keyed by partner id and tracks the shortfall/balance running tallies.
(defn agent [id type]
{:my-id id :type-of-academic type :age 0 :total-payoff 0 :memory {}})
Each round, both agents decide independently whether to offer H or L, then record the exchange:
(defn play-round
([a1 a2] (play-round a1 a2 default-params true))
([a1 a2 params pays-out?]
(let [act1 (decide a1 (:my-id a2) params)
act2 (decide a2 (:my-id a1) params)
[p1 p2] (typ/payoffs (:type-of-academic a1)
(:type-of-academic a2)
[act1 act2])]
[(record-exchange a1 (:my-id a2) act1 act2 p1 pays-out?)
(record-exchange a2 (:my-id a1) act2 act1 p2 pays-out?)])))
The decide function implements the reconsider rule: an agent plays its baseline until shortfall crosses the threshold, then a negative balance flips it to the opposite action (H-to-L or vice versa.)
(defn- reconsidering? [baseline shortfall threshold]
(and (= baseline :H) (>= shortfall threshold)))
(defn decide
[agent partner-id {threshold :change-of-strategy-threshold
:keys [reward-tick? reward-pct sanction-pct reward-maxes]}]
(let [t (:type-of-academic agent)
base (typ/baseline-action t)
m (get-in agent [:memory partner-id])
last (:last-action m base)]
(if (reconsidering? base (:difference-from-optimal m 0) threshold)
(let [rb (if reward-tick?
(reward-balance t (:recon-exch m {})
(reward-prob (:window-hh agent 0)
(:max-hh reward-maxes))
(reward-prob (:window-ll agent 0)
(:max-ll reward-maxes))
(double (or reward-pct 0))
(double (or sanction-pct 0)))
0.0)]
(if (neg? (+ (:balance m 0) rb))
(other last)
last))
base)))
The reward-tick? branch is used later: when a reward/sanction regime is active, the expected reward/sanction differential (rb) is added to the balance before the switch test.
I think it’s important to note, as in the paper, the experiments below are conducted on a fully-connected network of agents and each connection has a probability of collaboration. Obviously this doesn’t accurately reflect many organizations that may be more hierarchical or siloed. To that end they also model scale-free networks where agents have fewer connections, and they find those conditions more favorable for hs1.
Given a society of 20 agents hired evenly from hs1 and ls1: H actions begin around 20% of exchanges, then collapse to a ~9% steady state within the first decade or two and hold there across 1,500 simulated years.
Over 1,500 years (time axis is log-scale to show early collapse and the long flat tail.) Faint blue lines are each RNG seed's 5-year rolling mean; bold blue line is all-seed median.
The churn driving that collapse shows up in the career tenures and retirement reasons, by type. Plotting tenure-at-retirement, hs1 (left) piles up at short tenures while ls1 (right) generally lasts longer.
Splitting the same records by why each agent left is more telling. hs1 dominates the early-retirement side (bottom-% earners forced out under sustained low payoffs), while agents that survive to the mandatory retirement age reach it at similar tenures regardless of type. ls1’s longer careers come from rarely capitulating, not from outlasting hs1 once it does.
The paper’s finding holds across society sizes and strategy-change thresholds. (See original: 4.3)
Table 4 breaks that asymmetry by raising hs1’s LL payoff above ls1’s, so an hs1 stuck in mutual-low no longer trails its partner as it does under normal payoffs, yet the simulated outcome is mostly the same. (See original: 4.7)
The paper tests two basic strategies for preventing the collapse into low-quality exchanges:
The first lever is the mix of agent types in the society. The obvious move, hiring more hs1 agents, slows the decline but doesn’t stop it. (See original: 4.10)
As the paper notes:
Furthermore, it is quite challenging for a policy maker or employer to succeed in hiring such a high percentage of high-minded individuals.
What works better is changing the kind of high-minded agent you hire. These two non-selfish, high-minded types protect the H equilibrium:
Both types resist the capitulation mechanism entirely: since they always play H regardless, their shortfall/balance never push them to capitulate. Swap hs1 for either and the H-rate hovers around 50%.
Change the type, not the count: hs1 collapses to ~9%, while hn1/hn2 hold near 50%.
Under such conditions, the efficiency of an institution can be sustained if high-minded people are not selfish, we may call them “heroes” or “saints”.
While reliably hiring heroes (hn1) and saints (hn2) would evidently be more effective than hiring 70-90% high-minded, selfish agents (hs1), it is similarly challenging for most organizations. Mathematically, they cannot all “hire only the best.”
The second lever leaves the agent mix alone and changes the incentives (the reward-tick? branch of decide above).
As it turns out, rewarding HH exchanges has little effect, but sanctioning LL exchanges pulls capitulated hs1 agents back to H: the looming penalty enters the reconsider calculation and tips the balance.
Frequency also matters: sanctions every round push H-rates above 65%, while sanctions every three rounds barely help at all. Sanctioning LL is what helps. Frequent sanctions help more (bottom-left), and hiring more hs1 pushes the effect toward ~90% (bottom-right).
Sanctions only fire when an agent is in range of a reconsideration, so the more hs1 agents you start with, the more often the penalty has something to penalize. Hiring 65% hs1 lifts the baseline and reduces the sanction-frequency impact, recovering much of the sanction benefit even at the every-three-years cadence where 50/50 hiring collapses.
With selective hiring and balanced incentives, the H-rate climbs to ~90% (the most effective policy tested in the paper.)
These papers model societies/organizations that tend toward an equilibrium where low effort becomes pro-social behavior despite the undesirable outcomes. They contend “cartels of mediocrity” form against those offering higher quality exchanges.
To my mind, the big insight is that pro-social conformists and disillusioned try-hards (not free-riders) are the true drivers of decay: agents who’d genuinely prefer mutual excellence get captured by a social norm, “rationally” capitulating once sufficiently exploited. The high-minded burn out earlier and churn, and are replaced with new hires, keeping the organization flush with new subjects.
My advice to anyone finding themselves in scenarios resembling the ones above is to take pride in doing high-quality work even if peers do not! (But also, work within your means11. Don’t sacrifice your sanity and burn out.)
The Unaccountability Machine press.uchicago.edu
Also includes some history of cyberneticist and business-management consultant Stafford Beer12, who coined the phrase “the purpose of a system is what it does.” For a deeper review of the book, I recommend this blog, which also has a bunch of other interesting software posts. ↩
Wikipedia: Cybernetics
Note: the term cyber here is unrelated to computing. ↩
Wikipedia: Viable system model
A model of the organizational structure of any autonomous system capable of producing itself. ↩
Wikipedia: Law of requisite variety (cybernetics)
This felt immediately familiar to me from previous work on automation systems. ↩
Gambetta & Origgi, “The LL game: The curious preference for low quality and its norms” (2012) doi.org/10.1177/1470594X11433740 ↩
Proietti & Franco, “Social Norms and the Dominance of Low-Doers” (2018) jasss.org/21/1/6
Note: the PDF version appears to have accidentally duplicated Figure 2 as Figures 3 and 4 too. ↩
github.com/nextjournal/clerk Note: I initially wrote/developed this in a Clerk notebook, but this post is basically that notebook converted to Markdown with diagrams exported to SVG. ↩
There exists a fraudulent business in Southern Italy of adulterated olive oil made up mixing hazelnut and sunflower-seed oil, sold under the label “extra-virgin olive oil”. When Leonardo Marseglia – director of the Casa Olearia company in Apulia – was charged with contraband and fraud against European Union (and then acquitted) for having sold bogus oil under the label “extra virgin”, he justified himself in an interview by arguing that thanks to his adulterated oil many people could afford to buy oil with the label “extra virgin” at a reasonable price. Some people, he claimed, are interested in having at least the image of H-ness. “We pretend to buy good olive oil and you pretend to sell it”.
comses.net/codebases/5120 NetLogo source model from Social Norms and the Dominance of Low-Doers ↩
You must always work not just within but below your means. If you can handle three elements, handle only two. If you can handle ten, then handle five. In that way the ones you do handle, you handle with more ease, more mastery and you create a feeling of strength in reserve.
– Pablo Picasso
Wikipedia: Stafford Beer
Interesting aside: nearing retirement he wanted to pass the Cybernetics torch onto none other than Brian Eno, who went on to record hits like the Windows startup sound and some of my favorite albums. ↩
Babashka CLI is a library to write command line tools. It is available in babashka by default. This library was born out of some frustration with clojure -X&aposs functionality where people have to write raw EDN on the command line, which to me isn&apost a good user experience (especially not for people using Powershell where quoting rules are different than in bash and zsh). Babashka wants to give Clojure users a good scripting experience, no matter what OS or shell you are using.
While Babashka CLI had all the ingredients for parsing and formatting options (for help) and for multi-command (or subcommand) style (e.g. git remote show origin) invocations, you still had to write your own --help functionality. Also Babashka CLI didn&apost offer anything for getting shell completions. These two gaps existed as open Github issues for about three years now. What held me back in implementing these features was: A) I found help output for multi-command CLIs always a bit too opinionated. Every CLI I knew was doing it differently. Which one should I pick for bb.cli? and B) to implement shell completions I actually had to know something about shells I did not use personally. After looking at a couple of other libs like Howard M. Lewis Ship&aposs cli-tools, and Lambdaisland CLI and a couple more non-Clojure libraries, I decided I should just pick a help output that looks reasonable and offer an API to do your own thing if you want to do that. For implementing the completion support I re-used the branch that Sohalt and I worked on in 2024. Additionally I used Claude Code to get this work over the hump. Studying how Powershell or nushell completions work in detail just isn&apost that interesting to me and I was happy to defer most of the shell-specific nitty-gritty. One extra bonus feature is the nested command notation instead of the "table". This already existed in Babashka CLI for a while, but it&aposs now exposed for users.
The features described in this post are available as of Babashka CLI v0.11.73:
org.babashka/cli {:mvn/version "0.11.73"}
Let&aposs dig into an example to learn more about the new features!
Yeah, we&aposre going to write our own git, but don&apost worry, we&aposll not write our own VCS! We&aposll leave that up to Zach Oakes. Just the CLI interface this time and we&aposll let ourselves off the hook with println to fake the implementation. So here&aposs a bit of code for you to look at. There&aposs a bunch of functions like clone, log, checkout etc. that just print some info to stdout. The tree describes the command structure. And the dispatch call at the end dispatches the command line arguments over the tree.
#!/usr/bin/env bb
(require &apos[babashka.cli :as cli]
&apos[clojure.string :as str])
;; stand-ins; a real tool would shell out to git
(def ^:private branches ["main" "develop" "feature/login" "release/2.0"])
(def ^:private remotes ["origin" "upstream" "fork"])
(defn clone [{:keys [opts]}]
(println "Cloning" (:url opts)
(when (:depth opts) (str "(depth " (:depth opts) ")"))))
(defn log [{:keys [opts]}]
(println "Showing" (or (:max-count opts) "all") (name (:format opts)) "log entries"))
(defn checkout [{:keys [opts]}]
(println (if (:create opts) "Creating and switching to" "Switching to") (:branch opts)))
(defn remote-add [{:keys [opts]}]
(println "Added remote" (:name opts) "->" (:url opts)))
(defn remote-remove [{:keys [opts]}]
(println "Removed remote" (:name opts)))
(defn remote-list [_]
(run! println remotes))
(def tree
{:spec {:verbose {:coerce :boolean :desc "Be verbose" :alias :v}}
:cmd
{"clone"
{:fn clone :doc "Clone a repository into a new directory"
:spec {:url {:desc "Repository to clone from" :require true}
:depth {:desc "Create a shallow clone with N commits" :coerce :long}}
:args->opts [:url]}
"log"
{:fn log :doc "Show commit logs"
:spec {:format {:desc "Output format" :coerce :keyword
:validate #{:oneline :short :full}
:default :short}
:max-count {:desc "Limit the number of commits" :coerce :long :alias :n}}}
"checkout"
{:fn checkout :doc "Switch branches"
:spec {:branch {:desc "Branch to switch to" :coerce :string
:complete-fn (fn [{:keys [to-complete]}]
(filter #(str/starts-with? % to-complete) branches))
:require true}
:create {:desc "Create the branch before switching" :coerce :boolean :alias :b}}
:args->opts [:branch]}
"remote"
{:doc "Manage the set of tracked repositories"
:cmd-order ["add" "remove" "list"]
:cmd
{"add"
{:fn remote-add :doc "Add a remote"
:spec {:name {:desc "Remote name" :require true}
:url {:desc "Remote URL" :require true}}
:args->opts [:name :url]}
"remove"
{:fn remote-remove :doc "Remove a remote"
:spec {:name {:desc "Remote name" :coerce :string
:complete-fn (fn [{:keys [to-complete]}]
(filter #(str/starts-with? % to-complete) remotes))
:require true}}
:args->opts [:name]}
"list" {:fn remote-list :doc "List the existing remotes"}}}}
:epilog "Docs: https://example.com/mygit"})
(defn -main [& args]
(cli/dispatch tree args {:prog "mygit" :help true}))
(apply -main *command-line-args*)
The :prog value is used in help output and represents the program name. The :help true setting activates automatic help support. The automatic help support re-uses the already existing :desc (for options) /:doc (for commands) documentation values. When :validate is a set of keywords, auto-completion will pick up on this to autocomplete that option&aposs value.
Save this code as mygit.clj and make it executable.
chmod +x mygit.clj
Note that at the time of writing, Babashka CLI version 0.11.73 isn&apost part of the newly released bb yet. This is coming soon, but there&aposs more work to be done in babashka, to make babashka tasks even more awesome, which is going to be using part of the new CLI functionality. Stay tuned. For now you can add this snippet to the top of your code to make a bb script pick up on the newest CLI version:
(require &apos[babashka.deps :as deps])
(deps/add-deps &apos{:deps {org.babashka/cli {:mvn/version "0.11.73"}}})
(require &apos[babashka.cli] :reload)
Now we can invoke this script with ./mygit.clj. The usage line below will display mygit, because of the :prog setting, its display name, independent of how the script is invoked.
So let&aposs invoke it in a couple of different ways:
$ ./mygit.clj clone https://example.com/repo.git --depth 1
Cloning https://example.com/repo.git (depth 1)
$ ./mygit.clj checkout -b feature/login
Creating and switching to feature/login
$ ./mygit.clj log -n 5 --format oneline
Showing 5 oneline log entries
$ ./mygit.clj remote add origin https://example.com/repo.git
Added remote origin -> https://example.com/repo.git
The :help true option to dispatch enriches the command tree with --help / -h options at every level, including the top level of the tree. It will also include a terse error message when invalid command line options are provided. So this is the opinionated help support that you can use as a good default, but don&apost have to use if you want to do your own thing. When --help/-h is invoked explicitly, the exit code will be 0 and help output is printed to stdout. On invalid input, output is printed to stderr and the exit code will be 1.
This is what top level help output looks like: ./mygit.clj --help:
Usage: mygit [options] <command>
Commands:
clone Clone a repository into a new directory
log Show commit logs
checkout Switch branches
remote Manage the set of tracked repositories
Options:
-v, --verbose Be verbose
-h, --help Show this help
Run "mygit <command> --help" for more information on a command.
Docs: https://example.com/mygit
The per line description of a command comes from the :doc key, and the per line description of an option comes from the :desc key. Trailing prose can be provided via the :epilog key, which here is "Docs: https://example.com/mygit".
Every individual command also supports --help in a similar way:
Usage: mygit checkout [options] <branch>
Switch branches
Options:
--branch Branch to switch to (required)
-b, --create Create the branch before switching
-h, --help Show this help
Run "mygit --help" for global options.
Babashka CLI supports the :args->opts option to coalesce arguments into options. This is why we see <branch> printed as a supported argument. The (required) suffix comes from :require true in an option&aposs spec and the short -b comes from the :alias setting.
In our git implementation (unlike the real one), the remote command does not invoke a function on its own. It just provides a :doc value, describing what the group of child commands are for.
Running ./mygit.clj remote --help lists the group&aposs children:
Usage: mygit remote [options] <command>
Manage the set of tracked repositories
Commands:
add Add a remote
remove Remove a remote
list List the existing remotes
Options:
-h, --help Show this help
Run "mygit remote <command> --help" for more information on a command.
Run "mygit --help" for global options.
Invoking ./mygit.clj remote add --help shows the help of remote add, with both positional arguments in the usage line:
Usage: mygit remote add [options] <name> <url>
Add a remote
Options:
--name Remote name (required)
--url Remote URL (required)
-h, --help Show this help
Run "mygit --help" for global options.
A mistyped or missing command gives a terse error and exits with exit code 1:
$ ./mygit.clj statys
Unknown command: statys
Commands:
clone Clone a repository into a new directory
log Show commit logs
checkout Switch branches
remote Manage the set of tracked repositories
Run "mygit --help" for more information.
In Babashka CLI shell completions are produced dynamically by letting the shell call back into the CLI. As of today, Babashka CLI supports bash, zsh, fish, powershell and nushell.
We&aposre just going to show here how to get completions for zsh but the process is very similar for other shells.
The ./mygit.clj org.babashka.cli/completions snippet --shell zsh invocation spits out a zsh snippet to stdout specific to this CLI. The org.babashka.cli/completions is inserted by Babashka CLI.
To enable completions in zsh (after compinit), run:
source <(./mygit.clj org.babashka.cli/completions snippet --shell zsh)
This enables completions for commands and options, showing descriptions on the side. Already used options are not suggested again, unless they are expected to be used multiple times.
$ ./mygit.clj remote <TAB>
add -- Add a remote
remove -- Remove a remote
list -- List the existing remotes
The :validate set on log --format doubles as its completion source without adding extra config:
$ ./mygit.clj log --format <TAB>
full oneline short
Dynamic values can be supplied with :complete-fn. In our git example, branch names and remotes are completed by :complete-fn.
$ ./mygit.clj remote remove <TAB>
origin upstream fork
$ ./mygit.clj checkout <TAB>
main develop feature/login release/2.0
To see what the completer returns without a shell, you can call the completions command directly:
$ ./mygit.clj org.babashka.cli/completions complete --shell zsh -- remote &apos&apos
add Add a remote
remove Remove a remote
list List the existing remotes
--help Show this help
-h Show this help
After holding off and thinking about these issues for a couple of years, I finally bit the bullet and added help and completion support to Babashka CLI. Hope you&aposll enjoy it!
More exciting related stuff is coming soon. The new Babashka CLI will be integrated into babashka of course, but also babashka tasks will be pimped with automatic help and completions. I&aposm not yet done with that work though.
Meanwhile I&aposve been porting squint and neil over to the automatic help already.
A special shout-out to @lread for a ton of documentation review and improvements, and general maintenance. Thanks to @sohalt for the initial shell completions work back in 2024 that I picked up again for this release. Thanks to @plexus for his excellent Lambdaisland CLI talk at Babashka Conf 2026. Thanks also to Nextjournal whose commercial app I&aposm taking as a case study for this work, and last but not least to Clojurists Together and Sponsors on Github for giving me the time to work on this.
How would you go about rendering a scene reminiscent of dark teal water, lit from somewhere below, with thousands of faint cyan filaments drifting and swirling across it? Your instinct might be to reach for a shader or to create a particle simulation, but you could render the whole thing using just a couple hundred lines of arithmetic instead. That's precisely what we're going to do in this post by rendering these filaments using the same function Ken Perlin wrote in 1985 to fake textures on a computer that couldn't draw them for real, which we know today as Perlin noise.
I'll walk you through a moving-water visualization to illustrate what Perlin noise actually is, and how a single noise value can be used to steer thousands of particles into curving currents to create a flowing surface. The snippets use the Squint ClojureScript dialect, but the ideas are language-agnostic.
Naively using random values is the wrong approach for creating a natural-looking texture. Pure randomness at every pixel will produce boring static that's chaotic and grainy. Real surfaces such as marble or water are smooth because neighbouring points tend to be correlated. A piece of marble that's bright here is probably still fairly bright a millimetre over there.
Perlin noise provides a way to generate that kind of structured pseudo-randomness. It's a deterministic function from a point in space to a scalar value, with three properties that make it magical for graphics, which are as follows.
Nearby inputs give nearby outputs without seams, leading to smooth transitions. The same seed always gives the same output, so the texture ends up being stable across frames. And it has no preferred direction, making it look isotropic, unlike a simple grid of blurred random dots.
Under the hood it's just gradient noise generated in three steps. First, we need a tile space in the form of a grid, and then we plant a pseudo-random gradient vector at every corner to provide a direction. For any point inside a cell, we need to figure out how strongly each corner's gradient points toward it using a dot product. Finally, we just blend the contributions of the surrounding corners.
A naive linear blend would leave ugly visible creases at every grid line. Perlin, instead, passes the interpolation parameter through a fade curve which is a polynomial shaped so that it starts and ends flat, allowing the value to ease gently into each corner:
(defn fade [t]
(* t t t (+ (* t (- (* t 6) 15)) 10)))
The formula above is just 6t⁵ − 15t⁴ + 10t³ with its first derivative being zero at both t = 0 and t = 1, which is precisely what guarantees the output is smooth across cell boundaries. Linear interpolation itself is likewise dead simple:
(defn lerp [t a b]
(+ a (* t (- b a))))
The gradient lookup hashes a corner to one of a fixed set of directions and returns the dot product against the point's offset within the cell:
(defn grad [hash x y z]
(let [h (bit-and hash 15)
u (if (< h 8) x y)
v (if (< h 4) y (if (or (== h 12) (== h 14)) x z))]
(+ (if (zero? (bit-and h 1)) u (- u))
(if (zero? (bit-and h 2)) v (- v)))))
A small seeded PRNG shuffles an identity permutation table at construction time to decide which gradient each corner gets, making the field reproducible. A caller doesn't need to worry about any of this and simply passes their desired x, y, and z to noise3 to get back a smooth value. Perlin's raw output sits roughly in [-1, 1], and the implementation remaps it to [0, 1] so that downstream consumers can scale it linearly into their own positive range:
(/ (+ 1 n) 2)
And that's the whole noise engine in a nutshell. Now that we have our noise, let's see what we can do with it to create a smooth animation.
Smooth scalar values are nice, but what if we wanted to create an animation which moves in a particular direction? Well, to do that we just have to treat the noise value as an angle to give us a compass heading. Next, we multiply by a full turn (2π) so that the entire [0, 1] range maps to every possible direction:
(defn create-flow-field
[{:keys [noise noise-scale force-scale time-scale]
:or {noise-scale 0.003 force-scale 1 time-scale 0.15}}]
(let [noise3 (:noise3 noise)]
{:force-at
(fn [x y t]
(let [theta (* (noise3 (* x noise-scale) (* y noise-scale) (* t time-scale))
js/Math.PI 2)]
#js {:x (* (js/Math.cos theta) force-scale)
:y (* (js/Math.sin theta) force-scale)}))}))
And with that trick we get a flow field which we can ask for a velocity vector of a pixel at (x, y). Since the underlying noise is smooth, nearby pixels get nearly identical headings and the field ends up looking like a coherent map of currents, complete with eddies, calm spots, and converging streams.
The noise-scale knob controls the zoom factor of the flow. Scaling the coordinates down before sampling samples the noise at a coarse resolution, creating swirls that are broad and slow. On the other hand, scaling up produces nervous little vortices.
A keen reader will have noticed that the function takes a third coordinate, t, that we'll come back to later. For now, I'll leave a hint that it's going to be our secret ingredient for motion.
To actually see the current we have to drop particles into the field and let them drift. Each particle needs to keep track of its previous position as it's moved by its local current, so that we can draw a short line segment from where it was to where it landed:
(defn update-particle! [p force]
(set! (.-lifetime p) (dec (.-lifetime p)))
(if (neg? (.-lifetime p))
(respawn! p)
(do
(set! (.-prevX p) (.-x p))
(set! (.-prevY p) (.-y p))
(set! (.-x p) (+ (.-x p) (.-x force)))
(set! (.-y p) (+ (.-y p) (.-y force)))
(wrap! p (.-width p) (.-height p))))
p)
When we run that for a few thousand particles over a thousand frames in a row, they trace a curve through the field, and since the field is smooth and continuous, neighbouring particles trace neighbouring curves. The collective result has a look of flow lines following a current similar to the way dye disperses in moving water.
Each segment itself is just a stroked line, tinted by a second, finer noise pass so the colour shimmers across the surface instead of reading as just flat cyan:
(defn- draw-segment! [p noise2]
(let [v (noise2 (* (.-x p) 0.004) (* (.-y p) 0.004))
hue (+ 185 (* v 30))
light (+ 55 (* v 25))]
(set! (.-strokeStyle ctx) (str "hsla(" hue ", 80%, " light "%, 0.3)"))
(doto ctx
(.beginPath)
(.moveTo (.-prevX p) (.-prevY p))
(.lineTo (.-x p) (.-y p))
(.stroke))))
So the shape of the motion comes from the noise field while the colour comes from an independent one sampled at a different scale. Thus, we have two channels of the same primitive, doing two different jobs.
Everything we've done so far produces a frozen flow field. Next, we'll need to make two small changes to turn it into a living animation.
Remember the unused t in force-at, which we were going to come back to? Well, what I didn't mention is that Perlin noise can be defined for any number of dimensions, and the implementation here is actually 3D. The first two dimensions are in space, but the third one is time. Each frame, we advance t a tiny bit, and because the noise is smooth in all directions, the entire current field ends up drifting as a result. Eddies migrate, streams bend, while calm patches open and close. The field smoothly evolves from one frame to another as we increment the counter:
(swap! state update :time inc)
The time-scale parameter governs how fast that evolution happens, and we want to keep it small to produce gentle change rather than a strobe. And that's how using an extra noise dimension as the clock turns a static render into an animation. In case you're wondering, you can generalize it freely, and a 3D animation can be similarly created using 4D noise.
The last step is to make sure that our trails fade over time to create continuous motion as old trails fade out, and new ones appear over time. To achieve a shimmering effect we want to avoid fully clearing the canvas. Instead, every frame paints a translucent dark rectangle over the scene before drawing the new segments:
(set! (.-fillStyle ctx) "rgba(3, 18, 26, 0.03)")
(.fillRect ctx 0 0 width height)
A value of 0.03 alpha is doing a huge amount of work creating the effect of old line segments slowly getting drowned. A particle's recent trail glows bright, one from half a second ago starts to fade, and then it's gone completely. The result is a cheap, accidental motion-blur that gives the surface its reflective, continuously flowing quality.
Tuning this alpha number shifts the whole mood, with higher values making trails vanish almost instantly, while lower ones smear into long ghostly streaks.
Another thing to consider is how to keep the surface believable at the borders. Here, we can have particles that drift off one edge reappear on the opposite side using a toroidal, seamlessly tiling wrap. When a particle wraps, its previous position needs to wrap along with it to avoid drawing ugly streaks across the canvas:
(defn- wrap-delta [v extent]
(cond (>= v extent) (- extent)
(neg? v) extent
:else 0))
In a flow field, particles spiral into a handful of attractor orbits and drain out of the rest of the canvas. To keep the whole surface populated, each particle has to have a randomized lifetime so that when it expires, it can respawn at a fresh random location with its lifetime reset. Lifetimes also need to be jittered from the start so that respawns stay staggered rather than all firing on the same frame.
Here's how the whole machine runs frame by frame:
(defn draw []
(let [{:keys [width height particles time]} @state
force-at (:force-at field)
noise2 (:noise2 noise)
n (alength particles)]
;; Fade old trails toward the deep.
(set! (.-fillStyle ctx) "rgba(3, 18, 26, 0.03)")
(.fillRect ctx 0 0 width height)
(set! (.-lineWidth ctx) 1)
(set! (.-lineCap ctx) "round")
;; For each particle: sample the current, drift, draw its segment.
(dotimes [i n]
(let [p (aget particles i)]
(update-particle! p (force-at (.-x p) (.-y p) time))
(draw-segment! p noise2)))
;; Advance the clock and schedule the next frame.
(swap! state update :time inc)
(reset! raf-id (js/requestAnimationFrame draw))))
If you read the function from top to bottom, you can see the exact steps that are happening. First, a dark background is painted on the canvas, then the noise field is sampled for each of a couple thousand particles to see which way the water flows. The particles are nudged that way, and a faint coloured line is drawn behind each. Doing that sixty times a second creates the final animation.
Now we can see how the whole animation is put together. The noise gives us the basis for the flow field, the third dimension provides an arrow of time, and the fade creates a sense of motion. All of these ideas compose into something that feels far more complex than the sum of its parts.
The full source can be seen on Squint playground, and the version running above is served from perlin-flow.js generated from Squint.
The Bowling Game Kata is an oldie, but a goodie. The programming problem is to score a game of bowling. The more formal name of the game is Tenpin Bowling to distinguish it from other related games.
Here’s a link to the classic solution in Clojure by Stuart Halloway. The input for that version was simply a sequence of numeric rolls with no special marks.
For a little extra fun, I prefer a version that follows the standard notation for bowling, in which a game is represented as a string with an X for a strike, / for a spare, - for a gutter ball (no pins), and 1-9 for hitting so many pins. The game is played as ten frames, usually with two rolls per frame. However, as a special case, taking down all ten pins with the first ball results in a strike. The frame score for a strike is 10 plus the next two balls. If the first ball is less than 10, a second ball is rolled. If the second ball takes down the rest of the pins, you have a spare. The frame with a spare scores 10 plus the next ball. Any other combination of two balls in a frame scores the sum of the two balls. If the player scores a strike or spare in the tenth frame, he is allowed to roll extra balls as required to score the final frame. (The extra balls do not count as a new frame.) The final score is the sum of the ten frame scores.
A perfect game is all strikes “XXXXXXXXXXXX” – that’s twelve strikes for ten frames plus the two extra balls. The final score is 300.
The string notation also allows spaces to be used for readability. The spaces should be
ignored for scoring. Here’s a test that applies a score function. (The reader is welcome
to adapt it to his favorite test framework.)
(defn score-test [score]
(assert (= (score "35 6/ 7/ X 45 X X X XXXX") 223))
(assert (= (score "11 11 11 11 11 11 11 11 X 11") 30))
(assert (= (score "11 11 11 11 11 11 11 11 11 X 11") 30))
(assert (= (score "XXXXXXXXXXXX") 300))
(assert (= (score "9-9-9-9-9-9-9-9-9-9-") 90))
(assert (= (score "5/5/5/5/5/5/5/5/5/5/5") 150))
(assert (= (score "12 12 12 12 12 12 12 12 12 0/X") 47))
(assert (= (score "XX 3/ 4/ X 54 7/ X X X 3/") 198))
(assert (= (score "XX 3/ 4/ X 54 7/ X X X 37") 198))
true)
The problem statement does not require the detection of illegal inputs. That would be a good extension. For example, you can never have a spare in the first ball of a frame, and two balls in a frame cannot sum up to more than 10. My solution does not handle those errors. I am assuming valid input strings. (Famous last words.)
Most of the Clojure solutions I’ve seen, parse the string into a sequence of integers and
then do the appropriate calculations in a loop. My solution is a little different in that I
am parsing the string into a vector of integers and using a special value to mark a spare
(-1). The vector of balls representation allows me to use reduce-kv to drive the function
and have convenient access to other balls by offset from the current index. I should note
that reduce-kv over a vector gives you the index as the key. Clojure has some
optimizations that make reduce-kv fast. In this case, it’s especially handy to have an
index so that you can access nearby balls.
The mapping from a character digit into an integer is mostly done by converting to long and
subtracting (long \0) but with special cases for the X and -. It is a happy accident
that / maps into -1 in this scheme. That makes it convenient to test for a spare with the
neg? function.
The other trick I’m using is to count down from 20 “half frames” with special handling of
strkes, which count as 2 half frames. We need to count the frames in order to avoid
confusion with the possibility of extra balls for a mark in the final frame. When fc is
even, it means we’re looking at the first ball of a frame. The state of the reduction
function is a vector of the half-frame count and the running score, [fc sc]. We score a
frame only when it is complete (a strike or two balls). Clojure purists might prefer to use
a map for the state. For a small number of slots, positional values seem reasonable to
me. At then end, we only need the score so we call peek on the result of the reduction.
Using the reduce-kv over a vector of balls gives better performance than the common
sequence-oriented loop-style solutions. It is an eager approach. For a small problem
like this, there’s really no advantage to being lazy. One other performance note: using
clojure.string/replace to remove the spaces is faster than dealing with the spaces as
characters in a sequence. In general, the string functions are faster than I naively
expected, probably thanks to Java optimizations under the hood.
Here is my bowling score implementation.
(require '[clojure.string :as str])
(defn score [game]
(let [bv (mapv (fn [c] (let [x (- (long c) (long \0))] (case x 40 10 -3 0 x)))
(str/replace game " " ""))]
;; note X maps to 40, \ to -1, - to -3. The test for a spare mark is neg?
(peek
(reduce-kv (fn [[fc sc] i b]
(if (zero? fc)
(reduced [sc])
(if (even? fc)
;; first ball of frame
(if (= b 10) ;strike
(let [b2 (bv (+ i 2))]
[(- fc 2)
(if (neg? b2) (+ sc 20) (+ sc 10 (bv (inc i)) b2))])
;; don't score first ball, we will account for it on second ball
[(dec fc) sc])
;; second ball of frame, recover the previous ball if necessary
[(dec fc)
(if (neg? b) (+ sc 10 (bv (inc i))) (+ sc (bv (dec i)) b))])))
;; init at 20 half-frames, and zero score
[20 0]
bv))))
Methodology note: This article is a product of GenAI. The methodology imposed online research for generation, then multiple rounds of adversarial reviews and fixes until convergence, which needs 2 consecutive review-fix rounds with 0 issues found.
Analysis date: June 2026. All languages evaluated against their latest stable version as of this date.
Most language comparisons focus on syntax, performance, or popularity. This one is different.
We're analyzing each language through the Independent Variation Principle (IVP): how well does the language help you separate independent concerns and prevent unwanted coupling?
Version scope: Every language is assessed against its latest stable standard or release as of June 2026. Scores reflect modern idioms and current best practices for that version, not historical usage. Where a language has multiple widely-used versions with meaningfully different IVP properties, this is noted explicitly.
The Independent Variation Principle — Structural Formulation:
IVP-1 (Element Admissibility): Exclude any element that has no change driver — it contributes to no knowledge slice and has no reason to change.
IVP-2 (Change Driver Assignment): For each remaining element, assign every change driver whose knowledge slice the element realizes (an element implements part of a driver's knowledge slice iff that driver's changes require the element to change). If an element has more than one change driver, the multi-driver assignment must be irreducible (irreducible composite); a reducible composite is a design defect requiring decomposition.
IVP-3 (Unit Purity): Separate elements with different assigned sets of change drivers into distinct units.
IVP-4 (Unit Completeness): Unify elements with the same assigned set of change drivers within a single unit.
IVP decomposes all coupling in a system into two fundamental categories:
| Kind | Definition | Origin |
|---|---|---|
| Necessary coupling | Dependencies the system cannot function without — the irreducible coupling floor. Causal dependencies dictated by the domain structure. | Domain artifacts |
| Accidental coupling | Dependencies that are not causally required — coupling that exists by developer choice, oversight, or language/platform constraint. | Design or platform |
For each partitioning rule (change driver), necessary coupling is the minimum possible: the inter-class communication the domain demands plus the internal structure of correctly bounded modules. At maximal cohesion under all rules, coupling reduces to exactly necessary coupling plus any remaining accidental edges — the coupling-floor biconditional (Loth 2026, §Coupling as a Consequence of Cohesion, Thm. 1).
Accidental coupling subdivides into two subtypes, which are the core of this language analysis:
| Subtype | Definition | Fix |
|---|---|---|
| Avoidable accidental | Could be removed by better design within the language's capabilities. The developer chose to couple unnecessarily. | Better design |
| Language-imposed | Unavoidable given the language's constructs — the irreducible residuum of accidental coupling the language inflicts on all programs. Even optimal design cannot eliminate it. | Change language or version |
A language can also couple elements in dimensions beyond structural dependency. IVP identifies coupling dimensions: structural, temporal, concurrency, security, platform, and language (memory model, type system, module system). Each dimension carries its own dependency relation, and the eight-category decomposition applies per-rule, per-dimension — up to $8kr$ coupling categories for a system with $k$ rules and $r$ dimensions (Loth 2026, §Coupling Beyond Structural Dependency).
What this means for language comparison: This article evaluates languages along the language coupling dimension: how much language-imposed accidental coupling does the language generate, and how well does it help you detect and eliminate avoidable accidental coupling? A language with high IVP (a) minimizes language-imposed coupling (fewer constructs that force accidental edges) and (b) detects avoidable coupling at the earliest possible stage (compile time > static analysis > runtime > never).
This article's scores answer: how close does this language let you get to the irreducible coupling floor?
For each language, we examine:
"Meta-IVP" applies the IVP principle to broader language characteristics beyond code structure. These dimensions examine coupling at the ecosystem, tooling, and evolution level:
Each meta-IVP dimension receives a score (0-100) based on how well the language decouples these concerns.
Let's get into it.
Organization: Languages are grouped by paradigm and use case for easier comparison. Some languages appear in multiple conceptual categories (e.g., Java in both "Modern Mainstream" and "Enterprise Classics").
Need maximum safety? → Rust, SPARK, Lean4, Rocq, PureScript, Haskell, Ada
Formal verification? → SPARK, Lean4, Rocq (theorem provers)
Building enterprise apps? → C#, Java, Kotlin, TypeScript
Web development? → TypeScript, JavaScript, Python, Ruby, PHP
Systems programming? → Rust, C, D, Zig, C++
Data science/ML? → Python, R, Matlab, SQL
Scripting/automation? → Python, Bash, Zsh, Fish, PowerShell, Lua
Learning programming? → Scratch, Python, JavaScript, Scheme
Functional programming? → Haskell, OCaml, F#, Clojure, Scheme, PureScript, Unison
Game development? → C++, C#, Rust, D, Lua (scripting)
Mobile apps? → Swift (iOS), Kotlin (Android), C# (.NET MAUI)
Languages that enforce separation at compile time through strong type systems.
Type System & Abstraction:
map :: (a -> b) -> [a] -> [b] works for any types without knowing their internalsObject Construction:
Memory Management & Deallocation:
Error Handling:
Either Error Result makes error path explicit and separate from success pathhead, tail) can couple: Runtime errors bypass type system unless avoidedConcurrency:
IVP Violations:
IVP Tensions (What's Not IVP):
ReaderT (StateT (ExceptT ...)) ≠ StateT (ReaderT (ExceptT ...)), order mattersIVP Quality Score: 87/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 95 | Parametric polymorphism, type classes |
| Separation of Concerns | 90 | Pure functions, explicit effects |
| Construction Decoupling | 85 | ADTs, smart constructors |
| Error Handling | 85 | Either/Maybe, but partial functions exist |
| Concurrency | 90 | STM, immutability |
| Practical Usability | 70 | Steep learning curve, IO feels awkward |
Verdict: Haskell enforces IVP through its type system. Pure functions, explicit effects, and immutability make coupling visible and preventable. Main weaknesses: IO monad is too coarse-grained, lazy evaluation creates non-obvious coupling, monad transformers stack coupling.
Performance Characteristics: 65/100
Ecosystem & Tooling: 75/100
Language Evolution: 70/100
Domain Suitability: 70/100
Learning Curve: 50/100
Syntax & Ergonomics: 75/100
:: Monad m => (a -> m b) -> ... couples readability to type system knowledgef . g . h decouples steps<$>, <*>, >>=) couple readability to library knowledgeTesting & Debugging: 70/100
Security: 90/100
Meta-IVP Score: 71/100
Type System & Abstraction:
Object Construction:
Memory Management & Deallocation:
Error Handling:
Effect (console :: CONSOLE, exception :: EXCEPTION) Unit shows exactly what can failConcurrency:
IVP Violations:
IVP Tensions (What's Not IVP):
IVP Quality Score: 91/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 98 | Effect system superior to Haskell |
| Separation of Concerns | 95 | Algebraic effects separate effect types |
| Construction Decoupling | 85 | Same as Haskell |
| Error Handling | 90 | Explicit effect tracking |
| Concurrency | 92 | Aff monad, effect system |
| Practical Usability | 60 | Tiny community, JS interop friction |
Verdict: PureScript improves on Haskell's IVP by separating effect types. Effect system prevents coupling different concerns (console, network, state) into one IO bucket. Main weakness: JavaScript interop creates coupling escape hatches.
Performance Characteristics: 70/100
Ecosystem & Tooling: 60/100
Language Evolution: 65/100
Domain Suitability: 75/100
Learning Curve: 45/100
Syntax & Ergonomics: 80/100
foreign import couples JS interop to boilerplateTesting & Debugging: 65/100
Security: 85/100
unsafeCoerce and FFI bypass safety, couple correctness to disciplineMeta-IVP Score: 68/100
Type System & Abstraction:
Remote ability separates local from distributed concernsObject Construction:
Memory Management & Deallocation:
Remote computation handles serialization automaticallyError Handling:
{Exception} Result shows failure in type signatureConcurrency:
Concurrent, Remote abilities explicit in signaturesIVP Violations:
IVP Tensions (What's Not IVP):
Remote abstracts distribution but couples logic to serialization constraintsIVP Quality Score: 94/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 98 | Algebraic effects + content addressing |
| Separation of Concerns | 96 | Abilities separate effect types cleanly |
| Construction Decoupling | 92 | Immutability + content addressing |
| Error Handling | 95 | Algebraic effects make all errors explicit |
| Concurrency | 98 | Distributed computing built into language |
| Practical Usability | 55 | Small ecosystem, novel codebase management |
Verdict: Unison's content-addressed code and algebraic effects create exceptional IVP properties. Content addressing means functions are truly self-contained—no dependency on import paths, build order, or deployment infrastructure. Abilities provide better effect separation than Haskell's monolithic IO. Main weakness: immature ecosystem and learning curve for content-addressed paradigm.
Performance Characteristics: 65/100
Remote computation couples performance to serialization/networkEcosystem & Tooling: 50/100
Language Evolution: 70/100
Domain Suitability: 80/100
Learning Curve: 50/100
ucm makes exploration easierSyntax & Ergonomics: 85/100
Testing & Debugging: 70/100
Security: 90/100
Meta-IVP Score: 70/100
Type System & Abstraction:
Object Construction:
{ person with Age = 30 } doesn't modify originalMemory Management & Deallocation:
use keyword couples cleanupError Handling:
Result<'T, 'Error> makes errors explicitConcurrency:
async { } blocks separate concernsIVP Violations:
IVP Tensions (What's Not IVP):
IVP Quality Score: 78/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 80 | Good, but .NET nulls leak in |
| Separation of Concerns | 82 | FP-first, but OOP available |
| Construction Decoupling | 75 | Immutable by default, but .NET interop |
| Error Handling | 75 | Result type, but exceptions exist |
| Concurrency | 78 | Async, actors, but threads available |
| Practical Usability | 80 | Good tooling, .NET ecosystem |
Verdict: F# provides excellent IVP features (discriminated unions, Result, async, immutability) but .NET interop creates escape hatches. Pragmatic trade-off: IVP when you want it, C# compatibility when needed. Main weakness: discipline required to avoid .NET coupling traps.
Performance Characteristics: 80/100
Ecosystem & Tooling: 85/100
Language Evolution: 80/100
Domain Suitability: 85/100
Learning Curve: 70/100
Syntax & Ergonomics: 80/100
|> decouples data flowTesting & Debugging: 85/100
Security: 85/100
mutable, null, unsafe code possibleMeta-IVP Score: 81/100
Type System & Abstraction:
Object Construction:
Person("Alice", 30) instead of new Person(...)
Memory Management & Deallocation:
Error Handling:
for { a <- tryA; b <- tryB } yield f(a, b)
Concurrency:
Future[T] separates async from syncIVP Violations:
IVP Tensions (What's Not IVP):
IVP Quality Score: 76/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 85 | Powerful type system |
| Separation of Concerns | 75 | Traits, implicits, but complexity |
| Construction Decoupling | 78 | Case classes, apply methods |
| Error Handling | 70 | Try/Option/Either, but exceptions common |
| Concurrency | 85 | Futures, Akka actors |
| Practical Usability | 65 | Slow compilation, steep learning |
Verdict: Scala provides powerful IVP tools (traits, case classes, Future, implicits) but flexibility creates coupling opportunities. Implicits are double-edged: decouple parameter passing but create hidden dependencies. Main weakness: too many ways to do things means inconsistent IVP discipline.
Performance Characteristics: 75/100
Ecosystem & Tooling: 80/100
Language Evolution: 60/100
Domain Suitability: 85/100
Learning Curve: 40/100
Syntax & Ergonomics: 70/100
=>, <-, _, * used in many contextsTesting & Debugging: 75/100
Security: 80/100
null, exceptions can leak throughMeta-IVP Score: 71/100
Languages balancing IVP enforcement with practical ecosystem needs.
Type System & Abstraction:
string | number makes alternatives explicitany escape hatch destroys IVP: Can bypass all type checkingas Type forces couplingObject Construction:
{ name: "Alice", age: 30 } without classesMemory Management & Deallocation:
Error Handling:
throws clause, errors hidden from type systemneverthrow exist but uncommonConcurrency:
async function makes concurrency explicitPromise<T> doesn't show failure typeIVP Violations:
window, global create hidden couplingundefined is default, couples to absence handlingIVP Tensions (What's Not IVP):
any escape hatch couples strict code to untyped codeIVP Quality Score: 62/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 65 | Structural typing good, but any and runtime |
| Separation of Concerns | 68 | Interfaces, unions, but lots of escape hatches |
| Construction Decoupling | 60 | Object literals, but classes allow side effects |
| Error Handling | 45 | Unchecked exceptions, Promise rejection |
| Concurrency | 70 | Async/await, but error tracking weak |
| Practical Usability | 92 | Massive ecosystem, great tooling |
Verdict: TypeScript adds types to JavaScript but inherits JS's coupling problems. Structural typing is excellent for IVP, but unchecked exceptions, any, and type erasure create holes. Main strength: gradual typing lets you add IVP incrementally. Main weakness: type system doesn't enforce runtime behavior.
Performance Characteristics: 60/100
Ecosystem & Tooling: 90/100
Language Evolution: 85/100
@types packages couple to library versionsDomain Suitability: 90/100
Learning Curve: 70/100
Syntax & Ergonomics: 75/100
Testing & Debugging: 85/100
Security: 65/100
Meta-IVP Score: 78/100
Analysis against Java 24 (March 2025). Java 8 would score ~45; modern Java (17+) dramatically improves IVP properties.
Type System & Abstraction:
List<T> decouples from concrete typesequals/hashCode/toString — construction decoupled from mutationswitch expressions + sealed types enable exhaustive, compositional dispatchfinal or sealed)Object Construction:
record Point(int x, int y) — no mutation coupling, auto-generated componentsList.of(), Set.of(), Map.of() produce unmodifiable collectionsMemory Management & Deallocation:
Error Handling:
throws IOException makes errors explicitswitch + sealed types (Java 21): Exhaustive error handling — compiler verifies all error cases coveredConcurrency:
StructuredTaskScope — subtask lifetimes scoped to lexical block, preventing thread leakssynchronized, volatile, and manual lock patterns remain availableIVP Violations:
NullPointerException possible (no non-nullable types in the language)--illegal-access=deny (Java 17+, default) strictly limits reflective access to exported packages — reflection is no longer a free-for-allIVP Tensions (What's Not IVP):
IVP Quality Score: 71/100 (Java 24 with modern idioms — records, sealed types, virtual threads, pattern matching)
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 78 | Sealed classes, records, pattern matching; still null/erasure |
| Separation of Concerns | 75 | Interfaces, sealed hierarchies, modules, composition possible |
| Construction Decoupling | 72 | Records eliminate builder boilerplate; still null in standard library |
| Error Handling | 70 | Checked exceptions + sealed types + pattern matching improve composition |
| Concurrency | 78 | Virtual threads, structured concurrency, scoped values |
| Practical Usability | 90 | Massive ecosystem, mature tooling, 6-month release cadence |
Note: Java 8 would score ~45; Java 11 ~52; Java 17 ~60; Java 21 ~65. The 71 score reflects modern Java idioms: records over POJOs, sealed types over open hierarchies, virtual threads over thread pools, pattern matching over
instanceofchains.
Verdict: Modern Java (17+) significantly improves IVP over its reputation. Records provide immutability by default. Sealed classes enable controlled hierarchies. Virtual threads transform concurrency from callback soup to structured blocking. Pattern matching with sealed types enables exhaustive error handling. Module system restricts reflection. Main strength: interfaces + records + sealed types + virtual threads create a strong IVP toolkit. Main weakness: null still exists, type erasure persists, and legacy codebases may not adopt modern idioms.
Performance Characteristics: 82/100
Ecosystem & Tooling: 95/100
Language Evolution: 80/100
Domain Suitability: 85/100
Learning Curve: 68/100
Syntax & Ergonomics: 62/100
record Person(String name, int age) replaces 50-line POJOswitch expressions + sealed typesTesting & Debugging: 90/100
Security: 78/100
Meta-IVP Score: 80/100 (Java 24)
Analysis against C# 13 (.NET 9). C# 12/13 add primary constructors, collection expressions, params collections, and lock type.
Type System & Abstraction:
file access modifier restricts visibility to source file — IVP-3 unit purity at the file levelObject Construction:
new Person { Name = "Alice", Age = 30 }
[1, 2, 3] unified syntax decouples creation from collection typeMemory Management & Deallocation:
using statementError Handling:
throws clause, errors hidden?. operator reduces null couplingConcurrency:
System.Threading.Lock with pattern-based using scoping — decouples synchronization from object castingIVP Violations:
obj.Prop might do anythingIVP Quality Score: 72/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 78 | Generics better than Java, nullable types |
| Separation of Concerns | 75 | Extension methods, LINQ, interfaces |
| Construction Decoupling | 75 | Records, object initializers, DI |
| Error Handling | 55 | Unchecked exceptions |
| Concurrency | 85 | Excellent async/await, Task |
| Practical Usability | 88 | Great tooling, modern features |
Verdict: C# has evolved strong IVP features (extension methods, LINQ, records, async/await, nullable types) while maintaining .NET compatibility. Better than Java for IVP due to modern features. Main strength: async/await is IVP-perfect. Main weakness: unchecked exceptions hide failure coupling.
Performance Characteristics: 85/100
Ecosystem & Tooling: 95/100
Language Evolution: 90/100
Domain Suitability: 90/100
Learning Curve: 70/100
Syntax & Ergonomics: 75/100
Testing & Debugging: 95/100
Security: 80/100
unsafe keyword bypasses safetyMeta-IVP Score: 85/100
Type System & Abstraction:
String vs String? is explicitObject Construction:
val
by keyword separates interface from implementationMemory Management & Deallocation:
Error Handling:
Result<T> available but not widely used?. and ?: operatorsConcurrency:
suspend keyword makes async explicitIVP Violations:
IVP Quality Score: 79/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 85 | Null safety, sealed classes, generics |
| Separation of Concerns | 82 | Extension functions, sealed classes |
| Construction Decoupling | 83 | Data classes, default params, delegation |
| Error Handling | 65 | Unchecked exceptions, but null safety |
| Concurrency | 92 | Coroutines, structured concurrency |
| Practical Usability | 88 | Modern language, great Android support |
Verdict: Kotlin improves Java's IVP significantly: null safety, extension functions, coroutines all decouple concerns. Coroutines are among the best concurrency models for IVP. Main strength: null safety eliminates entire coupling class. Main weakness: unchecked exceptions, Java interop creates holes.
Performance Characteristics: 80/100
Ecosystem & Tooling: 90/100
Language Evolution: 85/100
Domain Suitability: 85/100
Learning Curve: 75/100
Syntax & Ergonomics: 85/100
? and !! clearTesting & Debugging: 85/100
Security: 85/100
Meta-IVP Score: 84/100
Type System & Abstraction:
<T: Equatable> decouples from concrete types?. syntax elegant but can hide nil propagationObject Construction:
init methods decouple constructioninit? makes construction failure explicitMemory Management & Deallocation:
Error Handling:
throws(MyError) makes error types explicit in signatures — a major IVP improvement over untyped throws
Result<T, E> available: For functional-style composable error handlinginit? returns nil instead of throwing! bypasses safety, couples to runtime crashConcurrency:
IVP Violations:
!): Bypasses type safety, couples to runtime crashesString! hides nil possibilityIVP Tensions (What's Not IVP):
IVP Quality Score: 85/100 (Swift 6 — typed throws, strict concurrency checking)
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 88 | Optionals, protocols, generics, strict Sendable |
| Separation of Concerns | 90 | Protocol-oriented design + typed throws |
| Construction Decoupling | 82 | Value types, initializers |
| Error Handling | 85 | Typed throws (Swift 6) makes error types visible in signatures |
| Concurrency | 92 | Actors, async/await, strict Sendable checking (on by default) |
| Practical Usability | 85 | Apple ecosystem, good tooling |
Verdict: Swift combines protocol-oriented programming with value semantics for excellent IVP. Swift 6 adds typed throws (error types in signatures) and strict concurrency checking (Sendable enforced by default), significantly improving both error handling and concurrency IVP. Main strength: value types + protocols decouple without inheritance; actors + strict Sendable prevent data races at compile time. Main weakness: Objective-C interop creates safety escape hatches, force unwrap bypasses type system.
Performance Characteristics: 85/100
Ecosystem & Tooling: 85/100
Language Evolution: 80/100
Domain Suitability: 80/100
Learning Curve: 70/100
Syntax & Ergonomics: 85/100
@available, @escaping can clutterTesting & Debugging: 85/100
Security: 85/100
Meta-IVP Score: 82/100
Type System & Abstraction:
Option<T> makes absence explicitObject Construction:
new() is convention, not specialMemory Management & Deallocation:
Rc<RefCell<T>> cycles require Weak to breakError Handling:
.unwrap()
? operator composes: Error handling doesn't obscure logicConcurrency:
IVP Violations:
IVP Tensions (What's Not IVP):
async fn creates two function worlds (sync/async), couples call sitesIVP Quality Score: 94/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 98 | Ownership, no null, no exceptions |
| Separation of Concerns | 95 | Traits, ownership prevents coupling |
| Construction Decoupling | 92 | Just functions, Drop trait |
| Error Handling | 98 | Result composes, no exceptions |
| Concurrency | 98 | Ownership prevents races |
| Practical Usability | 70 | Steep learning curve, borrow checker |
Verdict: Rust is designed around IVP. Ownership system enforces separation at compile time. Result types make errors explicit. Traits decouple without inheritance. Concurrency safety via ownership prevents temporal coupling. 2024 edition stabilized impl Trait in return position, async closures, and if let chains, further improving expressiveness. Main strength: compiler enforces IVP. Main weakness: learning curve, complex lifetimes couple code regions.
Performance Characteristics: 95/100
Ecosystem & Tooling: 90/100
Language Evolution: 85/100
Domain Suitability: 90/100
Learning Curve: 35/100
Syntax & Ergonomics: 70/100
<'a, 'b> syntax noisy! and #[] can be confusingTesting & Debugging: 80/100
Security: 98/100
Meta-IVP Score: 80/100
Analysis against Go 1.24. Generics have been stable since 1.18 (March 2022) — over 3 years of production use.
Type System & Abstraction:
interface{} casts — slices.Sort, maps.Clone, sync.Map type-safeiter.Seq[T] decouple iteration protocol from data structuremin/max/clear builtins (1.21): Built-in operations eliminate unnecessary function couplingObject Construction:
NewX() functions, not special syntaxinit() functions run at importMemory Management & Deallocation:
Error Handling:
func foo() (Result, error) makes errors visibleerrors.Join (1.20): Can combine multiple errors, improving composition slightlyConcurrency:
IVP Violations:
nil pointers, nil interfaces couple code to nil checks (no sum types to replace nil)IVP Quality Score: 76/100 (Go 1.24 — mature generics, range-over-func, slog)
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 73 | Implicit interfaces + generics eliminate casts; nil still a problem |
| Separation of Concerns | 85 | Composition, small interfaces, generics |
| Construction Decoupling | 75 | Zero values, convention-based constructors |
| Error Handling | 60 | Explicit but doesn't compose; errors.Join helps |
| Concurrency | 92 | Goroutines, channels excellent |
| Practical Usability | 92 | Simple, fast compilation, great tooling |
Verdict: Go's implicit interfaces are IVP-brilliant: decouple implementation from declaration. Goroutines/channels separate concurrency excellently. Generics (3+ years stable) eliminate interface{} coupling. Range-over-func enables custom iteration without coupling to built-in loops. Main strength: simplicity prevents complex coupling. Main weakness: nil everywhere, error handling doesn't compose, no sum types or pattern matching.
Performance Characteristics: 85/100
Ecosystem & Tooling: 90/100
Language Evolution: 95/100
Domain Suitability: 85/100
Learning Curve: 90/100
Syntax & Ergonomics: 75/100
if err != nil everywhereTesting & Debugging: 90/100
Security: 70/100
Meta-IVP Score: 85/100
Languages for performance-critical code and systems programming.
Analysis against C++23 (ISO/IEC 14882:2024). Older C++ standards have substantially worse IVP properties; C++23 represents the best of what modern C++ offers.
Type System & Abstraction:
import replaces #include, eliminating header couplingfinal specifier helps)Object Construction:
Memory Management & Deallocation:
unique_ptr, shared_ptr are idiomatic (manual new/delete is a code smell in modern C++)std::span (C++20): Non-owning view decouples algorithms from container typesstd::mdspan (C++23): Multi-dimensional views with bounds checkingstd::start_lifetime_as for safe low-level patternsshared_ptr has atomic ref counting cost: And circular references need weak_ptr
Error Handling:
std::expected (C++23): Monadic error handling — composable, type-safe, explicit in signaturesstd::optional monadic operations (C++23): and_then, transform, or_else enable composable absence handlingstd::expected is idiomatic and preferred, but exceptions are still in the language and used in the standard librarystd::stacktrace (C++23): Provides unwinding visibility, addressing historical debugging painstd::variant: Type-safe discriminated union for error discriminationConcurrency:
std::jthread (C++20): Auto-joining threads with cancellation via std::stop_token
std::latch, std::barrier, std::semaphore (C++20): Higher-level synchronization primitivesstd::generator (C++23): Coroutine-based generator decouples producer from consumerIVP Violations:
#include coupling for code not yet migrated to C++20 modulesIVP Tensions (What's Not IVP):
std::span bounds-checking is opt-in; raw pointer escape hatches remain for performanceIVP Quality Score: 67/100 (C++23 — modern idioms with smart pointers, expected, modules, concepts)
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 72 | Concepts, modules, spans, still UB escape hatches |
| Separation of Concerns | 72 | RAII, modules, concepts, but exceptions/macros remain |
| Construction Decoupling | 70 | Move semantics, designated initializers, but exception in ctor risk |
| Error Handling | 65 | std::expected composes, but exceptions still coexist |
| Concurrency | 58 | jthread, barriers, generator; still no compile-time race prevention |
| Practical Usability | 75 | Mature ecosystem, performance, legacy codebase availability |
Note: C++98/03 would score ~30; C++11/14 would score ~45; C++17 would score ~55. The 67 score reflects C++23 with modern idioms (smart pointers ≥ raw pointers,
std::expected≥ exceptions, modules ≥ headers, concepts ≥ unconstrained templates). Teams stuck on older standards or pre-modern coding styles will experience significantly worse IVP properties.
Verdict: C++23 provides substantially better IVP than its reputation suggests. Modules decouple compilation units. std::expected provides composable error handling. Concepts eliminate template error message hell. std::span/std::mdspan provide safe non-owning views. Smart pointers are idiomatic and eliminate manual new/delete. Main strength: RAII + zero-cost abstractions + deterministic performance. Main weakness: the language retains unsafe escape hatches (raw pointers, macros, exceptions) and new features coexist with old ones, meaning teams must enforce modern idiom discipline. C++23 with Core Guidelines and static analysis is a different language from C++98.
Performance Characteristics: 95/100
std::bitset, std::unique_ptr, std::optional, std::variant, std::string, cmath functions)Ecosystem & Tooling: 65/100
Language Evolution: 65/100
Domain Suitability: 90/100
Learning Curve: 25/100
expected, ranges, modules add to surfaceSyntax & Ergonomics: 60/100
import std; replaces many #includes<> remains noisy, though concepts improve readabilityauto, structured bindings (C++17): Reduce verbosityTesting & Debugging: 75/100
std::stacktrace (C++23): Runtime stacktrace capture improves debuggingSecurity: 55/100
std::span, std::mdspan, smart pointers are memory-safe; raw pointers and pointer arithmetic are notunique_ptr prevents use-after-free, double-freereinterpret_cast, pointer arithmetic, manual new/delete still compileMeta-IVP Score: 67/100 (C++23)
Type System & Abstraction:
Object Construction:
Memory Management & Deallocation:
Error Handling:
Concurrency:
IVP Violations:
IVP Tensions (What's Not IVP):
IVP Quality Score: 52/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 25 | Void pointers, no bounds checking, UB |
| Separation of Concerns | 65 | Explicit separation possible, discipline required |
| Construction Decoupling | 50 | Explicit, but manual and error-prone |
| Error Handling | 60 | Explicit return codes, but no enforcement |
| Concurrency | 30 | Manual, dangerous, UB on races |
| Practical Usability | 80 | Universal, proven, predictable |
Verdict: C provides no IVP enforcement but also no forced coupling mechanisms. Perfectly IVP-compatible IF disciplined: can separate concerns via function pointers, explicit error codes, manual memory management. Perfectly IVP-incompatible IF undisciplined: globals, macros, undefined behavior couple everything. Main insight: C is IVP-neutral—coupling is entirely up to programmer discipline. You're absolutely right that C can be perfect for IVP or terrible for IVP.
Performance Characteristics: 98/100
Ecosystem & Tooling: 70/100
Language Evolution: 85/100
Domain Suitability: 95/100
Learning Curve: 60/100
Syntax & Ergonomics: 60/100
int *(*fp)(void) confusingTesting & Debugging: 65/100
Security: 20/100
Meta-IVP Score: 69/100
Type System & Abstraction:
Object Construction:
Memory Management & Deallocation:
defer free(ptr) couples cleanup to scope, predictableError Handling:
!Type makes errors explicitunreachable must be markedConcurrency:
IVP Violations:
IVP Quality Score: 78/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 80 | Tagged unions, error unions, explicit |
| Separation of Concerns | 85 | No hidden control flow, comptime |
| Construction Decoupling | 88 | Allocator parameter, no constructors |
| Error Handling | 90 | Error unions compose, explicit |
| Concurrency | 60 | Still evolving, manual for now |
| Practical Usability | 65 | Pre-1.0, small ecosystem |
Verdict: Zig improves C's IVP by making everything explicit and adding error unions. Allocator-as-parameter decouples memory strategy brilliantly. Comptime separates compile-time from runtime. Main strength: no hidden control flow prevents subtle coupling. Main weakness: concurrency story incomplete, pre-1.0 instability.
Performance Characteristics: 95/100
Ecosystem & Tooling: 60/100
Language Evolution: 40/100
Domain Suitability: 85/100
Learning Curve: 65/100
Syntax & Ergonomics: 75/100
! syntax clearTesting & Debugging: 70/100
Security: 75/100
Meta-IVP Score: 71/100
Type System & Abstraction:
Object Construction:
Memory Management & Deallocation:
=destroy hook for custom cleanupalloc/dealloc available but error-proneError Handling:
Concurrency:
IVP Violations:
IVP Quality Score: 70/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 75 | Distinct types, concepts, but macros complex |
| Separation of Concerns | 73 | UFCS, multiple paradigms |
| Construction Decoupling | 70 | Multiple options, but GC couples |
| Error Handling | 65 | Exceptions exist, explicit options available |
| Concurrency | 68 | Multiple models, but GC complicates |
| Practical Usability | 62 | Small community, breaking changes |
Verdict: Nim provides flexible IVP support (distinct types, concepts, UFCS) but GC and macros create coupling. UFCS decouples method syntax from type definitions. Main strength: distinct types prevent accidental coupling (e.g., UserId vs Int). Main weakness: GC couples memory timing, exceptions less explicit than Result.
Performance Characteristics: 80/100
Ecosystem & Tooling: 55/100
Language Evolution: 65/100
Domain Suitability: 75/100
Learning Curve: 65/100
Syntax & Ergonomics: 75/100
Testing & Debugging: 65/100
Security: 65/100
Meta-IVP Score: 68/100
Languages prioritizing flexibility and rapid development over compile-time guarantees.
Type System & Abstraction:
Object Construction:
new keyword confusing: this binding couples to call contextnew entirelyMemory Management & Deallocation:
Error Handling:
.catch() optional(err, result) mixes concernsConcurrency:
IVP Violations:
window, globalThis everywherethis binding couples: Context depends on call siteIVP Tensions (What's Not IVP):
"5" + 3 === "53", "5" - 3 === 2).call(), .apply(), .bind() required)IVP Quality Score: 45/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 20 | No static types, runtime only |
| Separation of Concerns | 55 | Prototypes flexible, but globals everywhere |
| Construction Decoupling | 60 | Object literals good, new/this bad |
| Error Handling | 30 | Unchecked exceptions and Promise rejection |
| Concurrency | 65 | Async/await good, event loop implicit |
| Practical Usability | 95 | Ubiquitous, massive ecosystem |
Verdict: JavaScript's dynamic nature defers all coupling detection to runtime. Prototypes and object literals provide flexibility but no guarantees. Global scope and unchecked errors create pervasive coupling. Main strength: object literals and prototypes are simple. Main weakness: no type system means coupling invisible until runtime.
Performance Characteristics: 65/100
Ecosystem & Tooling: 95/100
Language Evolution: 80/100
Domain Suitability: 85/100
Learning Curve: 70/100
this, prototypes, coercionSyntax & Ergonomics: 75/100
[] + {} !== {} + []
Testing & Debugging: 85/100
Security: 55/100
Meta-IVP Score: 76/100
Analysis against Python 3.14 (free-threaded mode officially supported).
Type System & Abstraction:
match/case structural pattern matching (3.10+): Exhaustive dispatch on data structures, like Rust/Swift| union type syntax (3.10+): str | int cleaner than Union[str, int]
mypy/pyright provide enforcement at build timeObject Construction:
__init__ can have side effects: Constructors couple to initializationkw_only, slots options improve control@override decorator (3.12): Catches broken overrides at check timeMemory Management & Deallocation:
__del__ timing non-deterministic: Finalizers unreliable for resource cleanupwith statement provides deterministic cleanupError Handling:
except* (3.11+): Can handle multiple concurrent exceptions without losing informationexcept Exception catches too muchConcurrency:
--disable-gil build or python3.14t
IVP Violations:
def f(x=[]) couples calls togetherNone everywhere: Couples to absence checkingIVP Tensions (What's Not IVP):
mypy/pyright in CIdef f(x=[]) shares state across calls, couples invocationsIVP Quality Score: 53/100 (Python 3.14 — free-threaded mode, pattern matching, type checking tools)
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 30 | Dynamic, type hints via mypy/pyright are good but not enforced |
| Separation of Concerns | 62 | Duck typing + pattern matching flexible, but no compile enforcement |
| Construction Decoupling | 55 | Dataclasses help, inheritance couples |
| Error Handling | 38 | Unchecked exceptions, broad catching; exception groups help |
| Concurrency | 55 | Free-threaded mode (3.14+), async/await; GIL still default |
| Practical Usability | 90 | Huge ecosystem, readable syntax |
Note on concurrency: The IVP analysis gives Python partial credit for free-threaded mode (3.14+, officially supported). The GIL is still the default, but it's no longer a permanent restriction. Teams deploying free-threaded Python 3.14 can eliminate GIL-based threading coupling entirely. The 55 score reflects this transitional state; free-threaded-by-default Python would score ~65.
Verdict: Python's duck typing provides maximum runtime flexibility but zero compile-time IVP enforcement. Free-threaded mode (3.14) eliminates the historical GIL concurrency coupling, though it's still opt-in. Pattern matching (3.10+) and exception groups (3.11+) improve error handling composition. Type checkers (mypy/pyright) provide static IVP at build time but not at runtime. Main strength: simplicity, ecosystem, libraries. Main weakness: all coupling invisible until runtime; runtime type safety depends on test coverage.
Performance Characteristics: 48/100
Ecosystem & Tooling: 95/100
Language Evolution: 70/100
Domain Suitability: 90/100
Learning Curve: 95/100
Syntax & Ergonomics: 90/100
Testing & Debugging: 80/100
Security: 60/100
Meta-IVP Score: 77/100
Type System & Abstraction:
Object Construction:
$_GET, $_POST, $_SESSION coupleMemory Management & Deallocation:
__destruct runs when refcount hits zeroError Handling:
@ operator suppresses errors: Hides couplingConcurrency:
IVP Violations:
$_GET, $_POST, $_SESSION, $_SERVER everywherestrpos vs str_replace couples to memorization__get, __call obscure behaviorIVP Quality Score: 48/100 (PHP 8.4/8.5 — fibers, enums, readonly classes, property hooks, asymmetric visibility)
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 55 | Enums (8.1), readonly classes (8.2), property hooks (8.4), asymmetric visibility (8.4) — significant improvements; still weak vs static languages |
| Separation of Concerns | 55 | Interfaces/traits help, namespaces isolate; superglobals hurt |
| Construction Decoupling | 48 | Property hooks eliminate getter/setter boilerplate; superglobals persist |
| Error Handling | 35 | Unchecked, error/exception confusion unchanged |
| Concurrency | 42 | Fibers enable built-in async; still no standard event loop |
| Practical Usability | 88 | Ubiquitous for web, huge ecosystem |
Verdict: PHP is steadily improving IVP. Enums (8.1), readonly classes (8.2), property hooks and asymmetric visibility (8.4) add compile-time structure. Fibers (8.1) enable built-in async. Superglobals create pervasive coupling. Shared-nothing architecture prevents temporal coupling but wastes resources. Main strength: simplicity for web, rapid improvement cadence. Main weakness: superglobals, inconsistent stdlib, no standard async runtime.
Performance Characteristics: 60/100
Ecosystem & Tooling: 78/100
Language Evolution: 75/100
clone with, pipe operatorDomain Suitability: 72/100
Learning Curve: 80/100
$ prefix confusionSyntax & Ergonomics: 65/100
$_GET, $_POST everywhereTesting & Debugging: 72/100
Security: 52/100
Meta-IVP Score: 69/100 (PHP 8.4/8.5)
Analysis against Ruby 4.0 (December 2025). YJIT is production-ready since Ruby 3.2+.
Type System & Abstraction:
case/in provides exhaustive structural matching.rbs files) — static checking via Steep/Sorbet, not built into runtimeObject Construction:
initialize separate from new: Construction logic decoupledMemory Management & Deallocation:
Error Handling:
raise doesn't appear in signaturesrescue without type catches everything=> operator (3.0+): Can destructure error objects structurallyConcurrency:
IVP Violations:
$global creates pervasive coupling@@var shared across all instances, couples classesIVP Quality Score: 46/100 (Ruby 4.0 — pattern matching, Data.define, YJIT)
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 18 | Duck typing, RBS optional; pattern matching improves structure |
| Separation of Concerns | 58 | Mixins good, open classes bad |
| Construction Decoupling | 55 | Data.define adds immutability; still no enforcement |
| Error Handling | 28 | Unchecked, broad rescue; pattern matching helps |
| Concurrency | 38 | GVL persists; Fiber Scheduler + M:N threads improving |
| Practical Usability | 88 | Readable, productive, Rails ecosystem |
Verdict: Ruby prioritizes developer happiness over IVP enforcement. Pattern matching (3.0+) and Data.define (3.2+) add structured data handling. YJIT transforms performance. Main strength: readable, expressive syntax makes intent clear. Main weakness: all coupling detection deferred to runtime, metaprogramming creates invisible dependencies, GVL persists.
Performance Characteristics: 55/100
Ecosystem & Tooling: 90/100
Language Evolution: 72/100
Domain Suitability: 78/100
Learning Curve: 90/100
Syntax & Ergonomics: 95/100
Testing & Debugging: 85/100
Security: 55/100
Meta-IVP Score: 74/100
Type System & Abstraction:
$scalar, @array, %hash provide some structureObject Construction:
bless {} is manualMemory Management & Deallocation:
Error Handling:
eval catches everythingundef on error, silent failuresConcurrency:
IVP Violations:
$_, @_, %ENV everywhereIVP Quality Score: 32/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 10 | No types, weak typing |
| Separation of Concerns | 40 | Modules exist, but globals everywhere |
| Construction Decoupling | 25 | Bless is primitive, Moose helps |
| Error Handling | 20 | Die/eval, return undef |
| Concurrency | 30 | Threads discouraged, fork heavyweight |
| Practical Usability | 70 | CPAN is massive, regex powerful |
Verdict: Perl is pre-IVP era design. TIMTOWTDI creates inconsistent coupling patterns. Context sensitivity couples behavior to call site. Implicit $_ creates hidden dependencies. Main strength: CPAN ecosystem, regex power, system administration tasks. Main weakness: "write-only" code couples maintenance to original author's knowledge.
Performance Characteristics: 45/100
Ecosystem & Tooling: 75/100
Language Evolution: 50/100
Domain Suitability: 65/100
Learning Curve: 40/100
$_, context, referencesSyntax & Ergonomics: 30/100
$@% visual noise$!, $?, $@
Testing & Debugging: 70/100
Security: 50/100
Meta-IVP Score: 53/100
IVP Quality Score: 68/100 | Meta-IVP Score: 60/100
Core IVP: Systems language with GC (optional). Templates more readable than C++ (no angle bracket hell). Modules decouple better than C++ headers (no include guards). UFCS (Universal Function Call Syntax) decouples method calls. Contract programming (in/out/invariant) separates pre/postconditions. @safe, @trusted, @system attributes separate safety levels. Compile-time function execution (CTFE) decouples metaprogramming. Both GC and manual memory (BetterC mode). Multiple paradigms (imperative, OOP, functional, metaprogramming).
Meta-IVP: Better C++ philosophy (faster compilation, cleaner templates). Small ecosystem compared to Rust/C++. Niche adoption (not mainstream). Performance excellent (compiles to native). BetterC mode for embedded/no-GC. Learning curve moderate (familiar to C++ devs). Tooling improving (dub package manager). Syntax cleaner than C++ but similar power. Good for game development (vibe.d for web). Corporate backing weak. Community small but dedicated.
Shell languages for system administration and automation.
Type System & Abstraction:
Object Construction:
Memory Management & Deallocation:
Error Handling:
set -e to fail on errorstrap: Can handle signals, but limitedConcurrency:
& runs in backgroundIVP Violations:
local
IVP Quality Score: 25/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 0 | Everything is strings |
| Separation of Concerns | 30 | Functions exist, but globals everywhere |
| Construction Decoupling | 20 | No objects, manual everything |
| Error Handling | 25 | Exit codes, set -e, trap |
| Concurrency | 35 | Background jobs, pipes |
| Practical Usability | 75 | Ubiquitous, good for shell automation |
Verdict: Bash is a scripting language, not designed for IVP. Everything is strings, all variables global, errors ignored by default. Main strength: universally available, glues Unix tools together. Main weakness: couples to platform, environment, external commands. Use for small automation, not applications.
Performance Characteristics: 30/100
Ecosystem & Tooling: 60/100
Language Evolution: 60/100
Domain Suitability: 70/100
Learning Curve: 50/100
Syntax & Ergonomics: 40/100
[ vs [[, quoting rulesif [ "$x" = "y" ]; then
$?, $!, $@, $$
Testing & Debugging: 50/100
Security: 25/100
eval, command substitutionMeta-IVP Score: 48/100
Type System & Abstraction:
[Type] annotationsObject Construction:
[PSCustomObject] for ad-hocMemory Management & Deallocation:
Error Handling:
Concurrency:
IVP Violations:
$_ implicit like PerlIVP Quality Score: 64/100
| Dimension | Score | Notes |
|---|---|---|
| Type Safety | 70 | .NET types, but dynamic by default |
| Separation of Concerns | 75 | Modules, classes, cmdlets |
| Construction Decoupling | 65 | Objects, classes, but hashtables common |
| Error Handling | 70 | Try/catch, but terminating/non-terminating split |
| Concurrency | 60 | Jobs, workflows, runspaces |
| Practical Usability | 80 | Excellent for Windows administration |
Verdict: PowerShell is Bash done right: objects instead of strings, strong typing available, structured error handling. Main strength: .NET integration, pipeline with objects, module system. Main weakness: Windows-centric (improving with PowerShell Core), terminating/non-terminating error confusion.
Performance Characteristics: 55/100
Ecosystem & Tooling: 80/100
Language Evolution: 75/100
Domain Suitability: 85/100
Learning Curve: 65/100
Syntax & Ergonomics: 70/100
Get-ChildItem vs ls
| familiar but object-basedTesting & Debugging: 75/100
Security: 70/100
Meta-IVP Score: 72/100
IVP Quality Score: 32/100 | Meta-IVP Score: 58/100
Core IVP: Similar to Bash but better. Arrays indexed from 1 (like Lua). Better globbing decouples pattern matching. Associative arrays built-in. Still strings everywhere. Functions improved but global by default. Prompt theming decouples appearance. Parameter expansion more powerful but complex couples to expertise.
Meta-IVP: Excellent for interactive shells (oh-my-zsh). Better completion system than Bash. MacOS default (replaced Bash). Plugin ecosystem (frameworks) improves ergonomics. Faster than Bash for some operations. Compatibility mode for POSIX. Learning curve moderate (Bash knowledge transfers). Not as ubiquitous as Bash (less portable).
IVP Quality Score: 38/100 | Meta-IVP Score: 62/100
Core IVP: Modern rethink of shell design. Functions instead of aliases decouple better. No variable quoting needed (arrays are first-class). Configuration in separate files decouples from scripts. Autosuggestions decouple discovery. Still strings, but better structured. Not POSIX compatible couples to Fish-specific syntax.
Meta-IVP: Excellent for interactive use (out-of-box experience). Web-based configuration GUI. Syntax highlighting built-in. Fast completions. Small but growing ecosystem. Not POSIX = portability issues for scripts. Performance good (C++ implementation). Learning curve gentle (cleaner than Bash/Zsh).
IVP Quality Score: 28/100 | Meta-IVP Score: 50/100
Core IVP: Between Bash and classic sh. Associative arrays (ksh93). Better string manipulation than sh. Coprocesses for IPC. Floating-point arithmetic (unlike Bash). Still global variables everywhere. Better functions than sh. POSIX compliant + extensions.
Meta-IVP: Historical importance (influenced Bash). ksh93 most advanced. Used in commercial Unix (AIX, Solaris). Declining adoption (Bash dominates Linux). Performance competitive. Licensing was issue (now open source). Learning curve moderate. Less ecosystem than Bash/Zsh.
IVP Quality Score: 22/100 | Meta-IVP Score: 55/100
Core IVP: Minimal shell specification (POSIX standard). No arrays, no associative arrays. Everything is strings. Functions minimal. No local variables in original. Test with [ not [[. Most portable but least powerful. Global namespace pollution unavoidable.
Meta-IVP: Maximum portability (guaranteed on Unix/Linux). Dash commonly used as /bin/sh. Minimalist by design. Scripts run everywhere. Performance fast (minimal features). No interactive features. Learning curve gentle (few features). Used for boot scripts, system scripts where portability critical.
Classic languages maintained for legacy systems. Note: Full analyses for Java & C++ appear in sections 2 & 3.
Languages from the Lisp tradition emphasizing macros and symbolic computation.
IVP Quality Score: 68/100 | Meta-IVP Score: 58/100
Core IVP: Macros decouple syntax from semantics brilliantly. S-expressions uniform. CLOS multiple dispatch decouples behavior from single class hierarchy. Main coupling: dynamic typing, global functions, special variables create implicit dependencies.
Meta-IVP: Ancient ecosystem (1958), fragmented implementations (SBCL, CCL, etc.), slow evolution couples to legacy. Excellent for AI/symbolic computation, poor for modern web/systems. Steep learning curve (parentheses, macros), unique syntax. Security: memory safe but dynamic typing allows confusion.
IVP Quality Score: 55/100 | Meta-IVP Score: 52/100
Core IVP: Logic programming decouples "what" from "how" brilliantly. Unification separates pattern matching from execution. Backtracking couples execution order to query structure. Cut (!) creates global control flow coupling. Global predicates couple namespace.
Meta-IVP: Niche domain (logic programming, AI, NLP). Small ecosystem, limited libraries. Steep learning curve (declarative thinking). Poor performance for general computation. Excellent for constraint solving, rule engines. Fragmented (SWI-Prolog, GNU Prolog).
IVP Quality Score: 48/100 | Meta-IVP Score: 55/100
Core IVP: Array operations decouple iteration from computation. PURE functions enforce no side effects. Modules separate interfaces. COMMON blocks create global coupling nightmare. Implicit typing couples variables to naming convention (I-N are integers). GOTO couples control flow globally.
Meta-IVP: Excellent for numerical/scientific computing (domain fit). Legacy HPC code. Modern Fortran (2003+) improved modules, OOP. Aging ecosystem, declining adoption. Performance excellent (rivals C). Learning curve moderate, but legacy syntax quirks.
IVP Quality Score: 78/100 | Meta-IVP Score: 62/100
Core IVP: Strong typing prevents coupling via type errors. Packages separate interface/implementation. Tasking built-in for concurrency with rendezvous. Generic units decouple algorithms. Contracts (pre/postconditions) enforce separation. Verbose but explicit.
Meta-IVP: Designed for safety-critical systems (avionics, defense). Small ecosystem, niche adoption. Excellent for embedded/real-time. Steep learning curve (verbosity, tasking model). GNAT compiler mature. Security excellent (strong typing, contracts, no UB).
IVP Quality Score: 88/100 | Meta-IVP Score: 58/100
Core IVP: Ada subset with formal verification via proof. Flow analysis prevents information coupling (data dependencies explicit). Absence of runtime errors proven at compile-time decouples correctness from testing. No aliasing (no pointers) eliminates coupling through shared references. Contracts (pre/post/invariants) mathematically verified. Global variables require explicit declaration (data flow contracts). Separation of concerns proven, not just encouraged.
Meta-IVP: Extremely niche (highest-assurance systems: aviation, space, medical devices). Very steep learning curve (formal methods + Ada). Tooling excellent (GNATprove, SPARK Pro). Zero-defect software achievable. Performance same as Ada (compiled to native). Small ecosystem (subset of Ada's). Security maximum (proven absence of vulnerabilities like buffer overflows). Used where failure costs lives (Airbus, Thales, Lockheed Martin).
IVP Quality Score: 92/100 | Meta-IVP Score: 52/100
Core IVP: Theorem prover + functional programming language. Dependent types prevent coupling via mathematical proof (types can depend on values). Propositions-as-types (Curry-Howard) unifies proofs and programs. Separation of specification from implementation proven correct. Tactics decouple proof strategies. Monadic effect system separates pure/impure. Metaprogramming via macros decouples syntax. Type inference reduces annotation burden. Totality checking prevents infinite loops (all functions must terminate).
Meta-IVP: Primarily for theorem proving, formal verification, mathematics. Growing use for verified systems programming. Extremely steep learning curve (dependent types, proof techniques). Small but growing ecosystem. Excellent tooling (LSP, VS Code extension). Performance improving (compiles to C). Used in mathematics research (formalized proofs). Industry use rare (verification niche). Community academic but expanding. mathlib (standard library of mathematics) impressive.
IVP Quality Score: 90/100 | Meta-IVP Score: 50/100
Core IVP: Pure functional proof assistant. Dependent types decouple specifications from implementations (proven refinement). Inductive types separate data from proofs about data. Tactics language decouples proof construction strategies. Extraction generates OCaml/Haskell/Scheme from proofs (verified code generation). No side effects in logic core. Modules separate namespaces. Curry-Howard isomorphism unifies logic and computation. All functions total (termination required).
Meta-IVP: Primarily academic/research (formal verification, mathematics). Very steep learning curve (type theory, proof tactics). Small ecosystem (academic focus). Excellent for software verification (CompCert verified C compiler). Tooling improving (CoqIDE, Proof General, coq-lsp). Performance not primary concern (proof checking, not execution). Security maximum (mathematical certainty). Used for critical systems verification (cryptography, OS kernels). Community academic. Extraction to OCaml bridges to production.
IVP Quality Score: 84/100 | Meta-IVP Score: 68/100
Core IVP: Module system (functors, signatures) decouples brilliantly. Variant types (sum types) separate cases. Pattern matching explicit. Immutable by default. Objects optional (can violate IVP). Modules-as-values (first-class modules) powerful abstraction.
Meta-IVP: Excellent for compilers, static analysis, finance. Good performance (AOT compiled). Smaller ecosystem than Haskell/Rust. Excellent tooling (Merlin, dune). Learning curve moderate (easier than Haskell). Syntax can be terse. OCaml 5 adds parallelism.
IVP Quality Score: 72/100 | Meta-IVP Score: 75/100
Core IVP: Immutable data structures decouple state changes. Protocols decouple polymorphism from types. Atoms/Refs/Agents separate concurrency models. STM prevents coupling through shared state. Macros decouple syntax. Dynamic typing couples at runtime.
Meta-IVP: JVM ecosystem access. Excellent for web services, data processing. Good REPL-driven development. Learning curve steep (Lisp + functional + JVM). Slower startup (JVM). ClojureScript for frontend. Rich Hickey's philosophy (simple vs easy).
IVP Quality Score: 70/100 | Meta-IVP Score: 60/100
Core IVP: Language-oriented programming (create DSLs) decouples domain from implementation. Macros with syntax objects hygienic. Contracts enforce separation. Modules first-class. Dynamic typing couples at runtime. Continuation support powerful.
Meta-IVP: Excellent for education (DrRacket), PL research, DSLs. Small commercial adoption. Great for compilers, interpreters. Learning curve moderate. Scheme heritage. Growing ecosystem. Performance moderate (JIT).
IVP Quality Score: 65/100 | Meta-IVP Score: 52/100
Core IVP: Minimalist design decouples core from libraries. Continuations decouple control flow. Hygienic macros prevent namespace coupling. Dynamic typing couples at runtime. Global definitions couple namespace. Tail call optimization decouples recursion from stack.
Meta-IVP: Educational (SICP), research. Fragmented (R5RS, R6RS, R7RS). Small ecosystem. Excellent for teaching. Performance varies by implementation. Simple core, powerful abstractions. Niche adoption.
IVP Quality Score: 58/100 | Meta-IVP Score: 48/100
Core IVP: Everything is an object, message passing decouples sender from receiver. Live coding environment. Image-based development couples deployment to VM. Open classes create global coupling (like Ruby). No static typing couples to runtime.
Meta-IVP: Pure OOP (influenced Java, Ruby). Niche adoption today. Excellent for prototyping, live systems. Steep learning curve (image-based development). Security: memory safe, but image vulnerabilities. Performance moderate.
IVP Quality Score: 35/100 | Meta-IVP Score: 55/100
Core IVP: Everything is a string couples all operations to parsing. Upvar couples variable scopes. Global namespace pollution. Procs decouple procedures. Tk GUI toolkit tightly couples UI to Tcl. Minimal abstractions.
Meta-IVP: Excellent for embedded scripting (Tk, Expect, testing). Legacy adoption. Tcl/Tk for GUIs. Small ecosystem. Simple syntax. Performance slow (interpreted strings). Security: string-based = injection risks.
IVP Quality Score: 52/100 | Meta-IVP Score: 72/100
Core IVP: Tables are everything (arrays, dicts, objects) = flexible but couples structure. Metatables decouple behavior via __index. Coroutines decouple control flow. Dynamic typing couples at runtime. Global by default couples namespace. Simple, small core.
Meta-IVP: Excellent for game scripting (World of Warcraft, Roblox), embedded. Fast JIT (LuaJIT). Small footprint. Easy to embed in C. Learning curve gentle. Ecosystem moderate (LuaRocks). Performance excellent for scripting language.
IVP Quality Score: 62/100 | Meta-IVP Score: 58/100
Core IVP: Units separate interface/implementation. Properties decouple access. Events decouple GUI from logic. RAD components couple to VCL/FireMonkey. Inheritance couples class hierarchies. Exceptions unchecked. Pointers allow unsafe coupling.
Meta-IVP: Excellent for Windows desktop apps (legacy). Rapid Application Development (RAD). Declining adoption. Embarcadero ownership. Cross-platform (FireMonkey) improving. Learning curve gentle (Pascal heritage). Performance good (native compilation).
IVP Quality Score: 55/100 | Meta-IVP Score: 70/100
Core IVP: Similar to C# but more verbose. Interfaces decouple. Inheritance couples. WithEvents couples objects. Late binding (Option Strict Off) couples at runtime. On Error couples error handling to labels (legacy VB6).
Meta-IVP: Microsoft ecosystem. Declining in favor of C#. Excellent for Office automation, Windows forms. Learning curve gentle. .NET ecosystem access. Migration path to C#. Modern VB.NET better than VB6. Being deprecated by Microsoft.
Languages optimized for specific problem domains.
IVP Quality Score: 45/100 | Meta-IVP Score: 68/100
Core IVP: Vectorized operations decouple iteration. Data frames central abstraction. Functions first-class. Global environment couples namespace. Non-standard evaluation couples macros to context. Copy-on-modify semantics. Dynamic typing couples at runtime.
Meta-IVP: Excellent for statistics, data science. Massive package ecosystem (CRAN). RStudio excellent. Slower than Python (but improving). Domain-perfect fit. Steep learning curve (quirks: <-, factors, NSE). Memory intensive. Security: eval risks.
IVP Quality Score: 42/100 | Meta-IVP Score: 62/100
Core IVP: Matrix operations decouple iteration. Toolboxes decouple domains. Global workspace couples all variables. 1-indexed arrays couple to convention. Scripts pollute namespace. Functions in separate files couple organization. Cell arrays/structs weak typing.
Meta-IVP: Excellent for numerical computation, signal processing, control systems. Commercial license couples to cost. Simulink for modeling. Huge toolbox ecosystem. Learning curve moderate. GNU Octave open alternative. Performance good for matrix ops. Proprietary lock-in.
IVP Quality Score: 68/100 | Meta-IVP Score: 75/100
Core IVP: Declarative queries decouple "what" from "how". Schema separates structure from data. Views decouple presentation. Stored procedures couple logic to database. Triggers create implicit coupling. Vendor-specific SQL couples to DBMS. Normalization decouples data redundancy.
Meta-IVP: Perfect for data querying/manipulation. Universal adoption. Fragmentation (MySQL, PostgreSQL, Oracle, SQL Server). Standards (ANSI SQL) help but dialects differ. Performance excellent (query optimization). Learning curve gentle for basics, steep for optimization. Security: SQL injection major risk.
Languages for learning and unique use cases.
IVP Quality Score: 40/100 | Meta-IVP Score: 65/100
Core IVP: Visual blocks decouple syntax from logic. Event-driven decouples triggers from actions. Sprites couple behavior to visual objects. Global variables couple namespace. Broadcasts couple events globally. No types couples everything to runtime.
Meta-IVP: Excellent for education (children, beginners). Visual programming. Very limited for real applications. Massive adoption in K-12. Learning curve zero (drag-and-drop). No production use. Security: sandboxed. MIT Media Lab.
IVP Quality Score: 30/100 | Meta-IVP Score: 60/100
Core IVP: Complete control decouples from abstractions (none exist). Registers global state. Labels couple jump targets. Macros decouple repetition. Calling conventions couple ABI. Platform-specific couples to CPU architecture. No types couples everything.
Meta-IVP: Excellent for performance-critical kernels, embedded, reverse engineering. Steep learning curve (registers, instructions, ABI). No abstractions. Different syntaxes (AT&T vs Intel). Manual everything. Security: no protections, all vulnerabilities possible. Used where C too high-level.
Languages with unique IVP properties not covered by mainstream categories.
IVP Quality Score: 75/100 | Meta-IVP Score: 68/100
Core IVP: Actor model decouples processes by design — no shared memory, message passing only. "Let it crash" philosophy decouples error handling from business logic via OTP supervision trees. Pattern matching on binaries and messages exhaustively separates cases. Immutable data by default (no mutation coupling). Hot code swapping decouples deployment from runtime (upgrade without restarting). Dynamic typing couples at runtime (mitigated by Dialyzer static analysis). No null — atoms replace null. Per-process GC decouples memory management (no stop-the-world). BEAM VM provides preemptive scheduling — one slow process can't block others. Main weakness: dynamic typing, single-node focus (distribution is explicit).
Meta-IVP: Designed for telecom (Ericsson, 1986) — excellent for distributed, fault-tolerant systems (WhatsApp, RabbitMQ, Discord). OTP provides battle-tested patterns. BEAM VM mature (30+ years). Learning curve steep (functional + actor model + OTP). Small but dedicated ecosystem (Hex.pm, rebar3, mix). Elixir ecosystem accessible via Erlang interop. Performance excellent for I/O-bound, weak for CPU-bound. Concurrency model is the gold standard. Syntax unfamiliar (Prolog-derived).
IVP Quality Score: 78/100 | Meta-IVP Score: 75/100
Core IVP: Erlang BEAM with modern syntax. Macros via metaprogramming decouple syntax from semantics (compile-time code generation). Protocols decouple polymorphism from types (like Clojure). Pipe operator |> decouples data flow. Pattern matching exhaustive. Immutable data by default. OTP supervision trees decouple error recovery from logic. Phoenix LiveView decouples frontend state from JavaScript. Main weakness: dynamic typing (though Dialyzer + gradual typing improvements in progress via set-theoretic types).
Meta-IVP: Growing ecosystem (Phoenix, Nerves, Livebook). Excellent for real-time web (Phoenix Channels, LiveView). Hex.pm modern package manager. Mix build tool excellent. IDE support improving (ElixirLS). Learning curve moderate (Ruby-like syntax lowers barrier vs Erlang). Nerves for embedded. BEAM VM provides rock-solid foundation. Performance same as Erlang (BEAM). Documentation culture excellent. Community welcoming and growing.
IVP Quality Score: 65/100 | Meta-IVP Score: 62/100
Core IVP: Multiple dispatch decouples behavior from class hierarchy — functions are generic, methods are specialized by argument types. Type system is optional but powerful: parametric types, union types, abstract types all at runtime with JIT compilation. No inheritance (no hierarchy coupling). Metaprogramming via hygienic macros decouples syntax. Dynamic by default with optional type annotations. @code_warntype can detect type instability (accidental coupling). No null — Nothing and Missing types separate absence from value. 1-indexed arrays (like Fortran, R, Lua). Global scope coupling (mitigated by const and modules). Main weakness: time-to-first-plot (JIT warmup couples startup to latency), optional typing means undisciplined code has invisible coupling.
Meta-IVP: Excellent for scientific/numerical computing (competes with Python/Matlab but with native speed). Great metaprogramming capabilities. Growing ecosystem (JuliaHub, Pluto.jl). Very fast (approaches C for numerical code). Package manager (Pkg.jl) good. IDE support improving (VSCode extension). Learning curve moderate (multiple dispatch paradigm shift). REPL-driven development. Used in pharmaceuticals, finance, climate science. Performance: JIT warmup is the main pain point.
IVP Quality Score: 68/100 | Meta-IVP Score: 75/100
Core IVP: Sound null safety (Dart 2.12+) — String vs String? like Kotlin, enforced at compile time. Strong static typing with type inference. No checked exceptions (like C#). Async/await with Future<T> separates sync from async. Isolates decouple concurrency (no shared memory, message-passing only). Sealed classes (Dart 3) for exhaustive pattern matching. Records (Dart 3) for structural data. Pattern matching (Dart 3) decouples destructuring. Extension methods decouple behavior from types. late keyword for lazy initialization decouples initialization from construction. Main weakness: relatively small ecosystem outside Flutter, null safety migration still ongoing for old packages.
Meta-IVP: Excellent for cross-platform UI (Flutter). AOT compilation to native (iOS, Android) — no JIT overhead in production. JIT compilation for development (hot reload). Fast startup. Growing ecosystem (pub.dev). Google-backed. Learning curve gentle (familiar syntax to Java/TypeScript devs). Dart 3 unified the language (100% null safe). Used primarily for Flutter but expanding to server (Dart Frog, Shelf). Web compilation to JavaScript.
IVP Quality Score: 72/100 | Meta-IVP Score: 58/100
Core IVP: Ruby-like syntax with static typing and full type inference — write like Ruby, compile with Rust-like safety. No null — Nil is a type and must be handled explicitly (like Option). No dynamic typing (static at compile time). Compile-time macros (not runtime monkey-patching) decouple metaprogramming. Fibers for concurrency (like Go goroutines). Channels for CSP-style communication. Union types separate cases explicitly. No inheritance coupling (composition-focused). RAII-like resource management (deterministic). Main weakness: pre-1.0 wild period is over but language still gaining adoption, Windows support limited, single-threaded (fibers are cooperative).
Meta-IVP: Small but dedicated ecosystem. Excellent for CLI tools, web services (Kemal, Lucky). Performance very good (LLVM backend, compiled to native). Learning curve gentle (Ruby syntax lowers barrier for Ruby/Python devs). Shards package manager. Crystal 1.x is stable (since 2021). IDE support limited. Niche adoption. Community small but active.
| Language | IVP Score | Type Safety | Separation | Construction | Error Handling | Concurrency | Usability |
|---|---|---|---|---|---|---|---|
| Haskell | 87 | 95 | 90 | 85 | 85 | 90 | 70 |
| PureScript | 91 | 98 | 95 | 85 | 90 | 92 | 60 |
| Unison | 94 | 98 | 96 | 92 | 95 | 98 | 55 |
| F# | 78 | 80 | 82 | 75 | 75 | 78 | 80 |
| Scala | 76 | 85 | 75 | 78 | 70 | 85 | 65 |
| TypeScript | 62 | 65 | 68 | 60 | 45 | 70 | 92 |
| Java | 71 | 78 | 75 | 72 | 70 | 78 | 90 |
| C# | 72 | 78 | 75 | 75 | 55 | 85 | 88 |
| Kotlin | 79 | 85 | 82 | 83 | 65 | 92 | 88 |
| Rust | 94 | 98 | 95 | 92 | 98 | 98 | 70 |
| Go | 76 | 73 | 85 | 75 | 60 | 92 | 92 |
| C++ | 67 | 72 | 72 | 70 | 65 | 58 | 75 |
| C | 52 | 25 | 65 | 50 | 60 | 30 | 80 |
| D | 68 | 70 | 75 | 70 | 65 | 68 | 60 |
| Zig | 78 | 80 | 85 | 88 | 90 | 60 | 65 |
| Nim | 70 | 75 | 73 | 70 | 65 | 68 | 62 |
| JavaScript | 45 | 20 | 55 | 60 | 30 | 65 | 95 |
| Python | 53 | 30 | 62 | 55 | 38 | 55 | 90 |
| PHP | 48 | 55 | 55 | 48 | 35 | 42 | 88 |
| Swift | 85 | 88 | 90 | 82 | 85 | 92 | 85 |
| Ruby | 46 | 18 | 58 | 55 | 28 | 38 | 88 |
| Perl | 32 | 10 | 40 | 25 | 20 | 30 | 70 |
| Bash | 25 | 0 | 30 | 20 | 25 | 35 | 75 |
| Zsh | 32 | 5 | 35 | 30 | 28 | 40 | 80 |
| Fish | 38 | 15 | 42 | 35 | 35 | 45 | 85 |
| Ksh | 28 | 5 | 32 | 25 | 25 | 38 | 78 |
| Sh | 22 | 0 | 28 | 18 | 22 | 30 | 70 |
| PowerShell | 64 | 70 | 75 | 65 | 70 | 60 | 80 |
| Lisp | 68 | 60 | 75 | 70 | 65 | 70 | 65 |
| Prolog | 55 | 45 | 70 | 40 | 50 | 60 | 50 |
| Fortran | 48 | 50 | 55 | 45 | 40 | 50 | 60 |
| Ada | 78 | 90 | 82 | 75 | 75 | 80 | 60 |
| SPARK | 88 | 95 | 92 | 85 | 88 | 90 | 50 |
| Lean4 | 92 | 98 | 95 | 90 | 92 | 95 | 45 |
| Rocq | 90 | 98 | 92 | 88 | 90 | 92 | 40 |
| OCaml | 84 | 88 | 88 | 85 | 80 | 85 | 75 |
| Clojure | 72 | 55 | 80 | 75 | 70 | 75 | 75 |
| Racket | 70 | 50 | 75 | 72 | 68 | 70 | 70 |
| Scheme | 65 | 45 | 72 | 68 | 60 | 65 | 65 |
| Smalltalk | 58 | 40 | 65 | 60 | 55 | 60 | 55 |
| Tcl | 35 | 15 | 45 | 35 | 30 | 40 | 50 |
| Lua | 52 | 35 | 60 | 55 | 45 | 50 | 70 |
| Delphi | 62 | 65 | 70 | 60 | 55 | 65 | 70 |
| Visual Basic | 55 | 60 | 65 | 55 | 50 | 55 | 75 |
| R | 45 | 30 | 55 | 50 | 40 | 45 | 70 |
| Matlab | 42 | 35 | 50 | 45 | 38 | 42 | 65 |
| SQL | 68 | 70 | 75 | 70 | 65 | 68 | 75 |
| Scratch | 40 | 20 | 45 | 40 | 35 | 45 | 85 |
| Assembly | 30 | 10 | 35 | 25 | 20 | 30 | 40 |
| Erlang | 75 | 55 | 80 | 72 | 78 | 95 | 65 |
| Elixir | 78 | 55 | 82 | 75 | 80 | 95 | 72 |
| Julia | 65 | 62 | 72 | 60 | 58 | 68 | 68 |
| Dart | 68 | 80 | 72 | 70 | 55 | 75 | 78 |
| Crystal | 72 | 82 | 75 | 72 | 70 | 62 | 60 |
This table shows how each language handles ecosystem-level coupling concerns:
| Language | Meta-IVP Score | Performance | Ecosystem | Evolution | Domain | Learning | Syntax | Testing | Security |
|---|---|---|---|---|---|---|---|---|---|
| Haskell | 71 | 65 | 75 | 70 | 70 | 50 | 75 | 70 | 90 |
| PureScript | 68 | 70 | 60 | 65 | 75 | 45 | 80 | 65 | 85 |
| Unison | 70 | 65 | 50 | 70 | 80 | 50 | 85 | 70 | 90 |
| F# | 81 | 80 | 85 | 80 | 85 | 70 | 80 | 85 | 85 |
| Scala | 71 | 75 | 80 | 60 | 85 | 40 | 70 | 75 | 80 |
| TypeScript | 78 | 60 | 90 | 85 | 90 | 70 | 75 | 85 | 65 |
| Java | 80 | 82 | 95 | 80 | 85 | 68 | 62 | 90 | 78 |
| C# | 85 | 85 | 95 | 90 | 90 | 70 | 75 | 95 | 80 |
| Kotlin | 84 | 80 | 90 | 85 | 85 | 75 | 85 | 85 | 85 |
| Rust | 80 | 95 | 90 | 85 | 90 | 35 | 70 | 80 | 98 |
| Go | 85 | 85 | 90 | 95 | 85 | 90 | 75 | 90 | 70 |
| C++ | 67 | 95 | 65 | 65 | 90 | 25 | 60 | 75 | 55 |
| C | 69 | 98 | 70 | 85 | 95 | 60 | 60 | 65 | 20 |
| D | 60 | 90 | 55 | 60 | 75 | 65 | 70 | 70 | 70 |
| Zig | 71 | 95 | 60 | 40 | 85 | 65 | 75 | 70 | 75 |
| Nim | 68 | 80 | 55 | 65 | 75 | 65 | 75 | 65 | 65 |
| JavaScript | 76 | 65 | 95 | 80 | 85 | 70 | 75 | 85 | 55 |
| Python | 80 | 48 | 95 | 70 | 90 | 95 | 90 | 80 | 60 |
| PHP | 69 | 60 | 78 | 75 | 72 | 80 | 65 | 72 | 52 |
| Swift | 82 | 85 | 85 | 80 | 80 | 70 | 85 | 85 | 85 |
| Ruby | 77 | 55 | 90 | 72 | 78 | 90 | 95 | 85 | 55 |
| Perl | 53 | 45 | 75 | 50 | 65 | 40 | 30 | 70 | 50 |
| Bash | 48 | 30 | 60 | 60 | 70 | 50 | 40 | 50 | 25 |
| Zsh | 58 | 40 | 65 | 65 | 75 | 55 | 50 | 60 | 35 |
| Fish | 62 | 45 | 70 | 70 | 80 | 60 | 65 | 65 | 40 |
| Ksh | 50 | 35 | 62 | 62 | 72 | 52 | 45 | 55 | 28 |
| Sh | 55 | 50 | 75 | 75 | 85 | 50 | 35 | 60 | 30 |
| PowerShell | 72 | 55 | 80 | 75 | 85 | 65 | 70 | 75 | 70 |
| Lisp | 58 | 60 | 65 | 55 | 60 | 45 | 50 | 65 | 65 |
| Prolog | 52 | 40 | 50 | 50 | 70 | 40 | 45 | 55 | 65 |
| Fortran | 55 | 90 | 60 | 65 | 85 | 55 | 45 | 60 | 60 |
| Ada | 62 | 85 | 50 | 70 | 80 | 50 | 55 | 70 | 90 |
| SPARK | 58 | 85 | 40 | 70 | 85 | 30 | 50 | 75 | 98 |
| Lean4 | 52 | 75 | 45 | 65 | 75 | 25 | 60 | 70 | 95 |
| Rocq | 50 | 70 | 40 | 65 | 80 | 20 | 55 | 70 | 98 |
| OCaml | 68 | 80 | 65 | 70 | 75 | 60 | 70 | 75 | 80 |
| Clojure | 75 | 75 | 85 | 75 | 80 | 50 | 75 | 80 | 75 |
| Racket | 60 | 65 | 55 | 65 | 65 | 65 | 70 | 70 | 70 |
| Scheme | 52 | 60 | 45 | 55 | 55 | 70 | 65 | 60 | 65 |
| Smalltalk | 48 | 55 | 40 | 55 | 60 | 50 | 60 | 55 | 65 |
| Tcl | 55 | 40 | 60 | 60 | 65 | 60 | 45 | 55 | 50 |
| Lua | 72 | 80 | 70 | 70 | 85 | 85 | 80 | 70 | 70 |
| Delphi | 58 | 75 | 55 | 60 | 70 | 70 | 65 | 65 | 65 |
| Visual Basic | 70 | 70 | 85 | 75 | 75 | 75 | 65 | 75 | 70 |
| R | 68 | 50 | 85 | 65 | 95 | 60 | 70 | 75 | 60 |
| Matlab | 62 | 75 | 70 | 65 | 90 | 65 | 60 | 70 | 60 |
| SQL | 75 | 85 | 90 | 80 | 95 | 75 | 70 | 75 | 65 |
| Scratch | 65 | 50 | 60 | 70 | 85 | 100 | 80 | 60 | 70 |
| Assembly | 60 | 98 | 65 | 75 | 90 | 20 | 35 | 55 | 15 |
| Erlang | 68 | 70 | 65 | 70 | 80 | 40 | 60 | 75 | 80 |
| Elixir | 75 | 70 | 78 | 75 | 82 | 55 | 82 | 80 | 80 |
| Julia | 62 | 78 | 65 | 60 | 85 | 55 | 75 | 60 | 70 |
| Dart | 75 | 72 | 82 | 78 | 82 | 72 | 75 | 78 | 70 |
| Crystal | 58 | 82 | 55 | 60 | 65 | 70 | 78 | 55 | 72 |
Performance (Coupling to Runtime Behavior)
Ecosystem & Tooling (Coupling to Build Systems/Libraries)
Language Evolution (Coupling to Language Versions)
Domain Suitability (Coupling to Problem Domains)
Learning Curve (Coupling Productivity to Expertise)
Syntax & Ergonomics (Coupling Intent to Verbose Expression)
Testing & Debugging (Coupling Understanding to Manual Inspection)
Security (Coupling Correctness to Security Concerns)
| Language | Core IVP | Meta-IVP | Overall IVP Quality |
|---|---|---|---|
| Rust | 94 | 80 | ⭐⭐⭐⭐⭐ Maximum Enforcement |
| Unison | 94 | 70 | ⭐⭐⭐⭐⭐ Content-Addressed + Algebraic Effects |
| Lean4 | 92 | 52 | ⭐⭐⭐⭐⭐ Theorem Proving |
| PureScript | 91 | 68 | ⭐⭐⭐⭐⭐ FP Excellence |
| Rocq | 90 | 50 | ⭐⭐⭐⭐⭐ Formal Verification |
| SPARK | 88 | 58 | ⭐⭐⭐⭐⭐ Proven Correctness |
| Haskell | 87 | 71 | ⭐⭐⭐⭐⭐ Academic Excellence |
| Swift | 85 | 82 | ⭐⭐⭐⭐⭐ Protocol-Oriented + Typed Throws |
| OCaml | 84 | 68 | ⭐⭐⭐⭐⭐ Module System Excellence |
| Kotlin | 79 | 84 | ⭐⭐⭐⭐⭐ Modern JVM |
| F# | 78 | 81 | ⭐⭐⭐⭐ Pragmatic FP |
| Ada | 78 | 62 | ⭐⭐⭐⭐ Safety-Critical |
| Zig | 78 | 71 | ⭐⭐⭐⭐ Explicit Systems |
| Scala | 76 | 71 | ⭐⭐⭐⭐ Complex But Powerful |
| Go | 76 | 85 | ⭐⭐⭐⭐⭐ Simplicity + Stability (Generics Mature) |
| C# | 72 | 85 | ⭐⭐⭐⭐⭐ Best Balance |
| Clojure | 72 | 75 | ⭐⭐⭐⭐ Immutable JVM |
| Java | 71 | 80 | ⭐⭐⭐⭐⭐ Enterprise Workhorse (Modern) |
| Racket | 70 | 60 | ⭐⭐⭐ Language-Oriented |
| Nim | 70 | 68 | ⭐⭐⭐ Python + Performance |
| Lisp | 68 | 58 | ⭐⭐⭐ Macro Power |
| SQL | 68 | 75 | ⭐⭐⭐ Declarative Data |
| D | 68 | 60 | ⭐⭐⭐ Better C++ |
| C++ | 67 | 67 | ⭐⭐⭐⭐ Modern Power (C++23) |
| Scheme | 65 | 52 | ⭐⭐⭐ Minimalist |
| PowerShell | 64 | 72 | ⭐⭐⭐ Objects > Strings |
| TypeScript | 62 | 78 | ⭐⭐⭐ JavaScript + Types |
| Delphi | 62 | 58 | ⭐⭐⭐ RAD Legacy |
| Smalltalk | 58 | 48 | ⭐⭐⭐ OOP Pioneer |
| Visual Basic | 55 | 70 | ⭐⭐⭐ Office Automation |
| Prolog | 55 | 52 | ⭐⭐⭐ Logic Programming |
| Python | 53 | 80 | ⭐⭐⭐⭐ Productivity King (Free-threaded) |
| Lua | 52 | 72 | ⭐⭐⭐ Embedded Scripting |
| C | 52 | 69 | ⭐⭐⭐ IVP Neutral |
| Fortran | 48 | 55 | ⭐⭐ Scientific Legacy |
| PHP | 48 | 69 | ⭐⭐ Web + Modernizing |
| Ruby | 46 | 77 | ⭐⭐⭐ Developer Happiness (YJIT) |
| R | 45 | 68 | ⭐⭐ Statistics Domain |
| JavaScript | 45 | 76 | ⭐⭐⭐ Ubiquitous |
| Matlab | 42 | 62 | ⭐⭐ Engineering Domain |
| Scratch | 40 | 65 | ⭐⭐ Education First |
| Fish | 38 | 62 | ⭐⭐ Friendly Shell |
| Tcl | 35 | 55 | ⭐⭐ Embedded Legacy |
| Zsh | 32 | 58 | ⭐⭐ Enhanced Shell |
| Perl | 32 | 53 | ⭐⭐ TIMTOWTDI |
| Assembly | 30 | 60 | ⭐ Maximum Control |
| Erlang | 75 | 68 | ⭐⭐⭐⭐ Actor Model Gold Standard |
| Elixir | 78 | 75 | ⭐⭐⭐⭐ Modern BEAM |
| Julia | 65 | 62 | ⭐⭐⭐ Scientific + Multiple Dispatch |
| Dart | 68 | 75 | ⭐⭐⭐ Null Safety + Flutter |
| Crystal | 72 | 58 | ⭐⭐⭐ Ruby Syntax, Static Types |
| Ksh | 28 | 50 | ⭐ Korn Shell |
| Bash | 25 | 48 | ⭐ Shell Glue |
| Sh | 22 | 55 | ⭐ POSIX Shell |
By Paradigm:
By Score (latest version, modern idioms):
All scores reflect the latest stable version and modern idioms as of June 2026. Older versions of the same language can score significantly lower (e.g., C++98 ≈ 30, Java 8 ≈ 45).
If you need mathematical proof of correctness: Lean4 (92), Rocq (90), SPARK (88)
If you want bulletproof IVP enforcement: Rust (94), PureScript (91), Haskell (87), OCaml (84)
If you want IVP with pragmatism: Swift (85), Elixir (78), Kotlin (79), Zig (78), F# (78), Go (76), C# (72)
If you want discipline-required IVP (modern versions): Java (71), Crystal (72), C++ (67—C++23), Scala (76), D (68), Dart (68), Nim (70), TypeScript (62)
IVP on a budget (ecosystem-driven): Python (53), Julia (65), C (52—can be great or terrible), PHP (48), Ruby (46), JavaScript (45)
Specialized excellence (unique IVP models): Erlang (75): Actor model with per-process isolation and let-it-crash. Elixir (78): Modern BEAM with macros and protocols. Julia (65): Multiple dispatch without inheritance.
The IVP Champions:
The IVP Pragmatists (latest versions):
The Modernized (significant score increases with recent versions):
The IVP Hazards:
The IVP Neutral:
IVP scores depend heavily on which version of a language you're using. C++23 is a fundamentally different language from C++98. Java 24 with records, sealed classes, and virtual threads is not the same as Java 8. Always check the version baseline.
Version aside, IVP scores don't determine project success. Context matters:
But here's what IVP analysis reveals:
Languages with proven separation (Lean4, Rocq, SPARK) mathematically guarantee correctness. Extreme learning curve, but zero-defect systems achievable.
Languages with enforced separation (Rust, Haskell, PureScript, OCaml) make coupling impossible to hide. You pay upfront in learning curve, gain long-term in maintainability.
Languages with optional separation (Swift, Kotlin, C#, TypeScript, modern Java, modern C++) let you choose IVP level. Good for migration, requires discipline.
Languages with manual separation (C, D, Zig) give you rope to hang yourself or build elegantly. Expertise required.
Languages with runtime-only separation (JavaScript, Python, PHP, Ruby, shells) defer most coupling detection to testing — but Python 3.14 (free-threaded), PHP 8.4 (property hooks, fibers), and Ruby 4.0 (YJIT, pattern matching) have all made meaningful IVP progress in recent versions.
The lesson: Match language IVP capabilities to your context AND your version. C++23 with modern idioms is a different beast from C++98. Java 24 with virtual threads, records, and sealed classes is not Java 8. Don't pick a language based on its worst version. Building life-critical systems? Use SPARK or Lean4. Building a 20-year system with changing requirements? Invest in Rust. Building mobile apps? Swift or Kotlin. Building a weekend project? JavaScript is fine. Maintaining legacy code? TypeScript adds IVP incrementally. Need shell automation? Fish or PowerShell beat Bash.
Stop chasing the perfect language. Pick one that fits your constraints, understand its coupling traps, and build with discipline.
The language wars are over. IVP shows why: different contexts need different coupling trade-offs.
Now get back to building things.
The Independent Variation Principle is introduced in:
Loth, Y. (2025). The Independent Variation Principle. Zenodo.
https://doi.org/10.5281/zenodo.17677316
The paper provides:
Which language's IVP analysis surprised you? Did I miss critical coupling mechanisms? Share your experiences in the comments!
I'm releasing another Biff 2 library: biff.fx. It's a lightweight approach to removing effects from your application logic, which makes that logic easier to understand, test, and reuse.
There are basically two ideas here. First is the common approach of having your
code return data describing effects (http requests, database
queries/transactions, etc) it wants to run instead of running those effects
directly. So for example, instead of calling (http/request "https://example.com" {:query-params {:foo "bar"}}), you would return a vector
like [:my-application.fx/http "https://example.com" {:query-params {:foo "bar"}}], and then some sort of orchestrator would call http/request for you.
Then it's easy to unit-test your code since it's pure, and if you wanted to swap
out certain effect implementations when running integration tests, that's easy
to do too.
You could set something like that up with Ring middleware where effects run before and after the handler. Handler functions could somehow declare what input data they need/what database query they need to run, if any, and then they could return some effect data for any database transactions etc they need to do afterward.
That works as long as you can structure your logic and effects like a sandwich, with effects on the outside and gooey, pure logic on the inside. What if you need logic on either side of an effect though, i.e. what if you need to interleave logic and effects? For example, in one of my apps I have some code to initialize a Stripe checkout session. It has to:
The database bits can be pushed before and after our logic, however the HTTP requests can't. So what do we do?
One approach would be to just have a special case for situations like this under the assumption that most of your code can in fact be structured as a sandwich. i.e. write a plain-old-impure-function and be on with your day.
Another approach is to use data not just to describe effects but also to describe control flow. One of the shapes that approach can take is:
i.e. you make a state machine where the states are pure logic and effects happen in the transitions. And that's what biff.fx does.
Plug: my team is hiring for a senior software engineer, writing ClojureScript and Python mostly. We make modeling software for renewable energy projects.
Great news, everyone - CIDER 1.22 (“São Miguel”) is finally out!
And “finally” is the operative word here. This release took me way longer than I wanted it to, but that’s because I decided to stop kicking a few cans down the road and finally tackle some long-standing problems that had been bugging me for years:
None of this is the kind of work that makes for a flashy release announcement, but it’s exactly the kind of work that keeps a 14 year old project healthy. I genuinely think this is one of the most important CIDER releases in recent memory, even though most of the changes aren’t really user-visible.
Picking a handful of items out of a very long changelog, here’s what I’d call the highlights:
cider-set-default-session lets you bypass sesman’s project-based dispatch and pin a REPL as the default. Handy for all those workflows that never quite fit the project model.cider-connect completions no longer re-spawn a round of ps/lsof subprocesses every single time.C-h m now lists the active bindings for several modes. A lot of functionality that was technically there but practically invisible is now front and center.cider-repl-history-doctor (#3921) - a new command that walks your REPL history, finds entries with unbalanced parens, and helps you clean them up. Born out of a real bug report about history rendering breaking.nREPL to 1.7.0 and cider-nrepl to 0.59.0.The full list is much longer - check out the changelog if you want the gory details.
While I was doing the nREPL decoupling work, I got curious and started experimenting with adding support for prepl (Clojure’s built-in socket REPL) as an alternative to nREPL. I even put together a prototype (cider#3899). It sort of worked, but it also reaffirmed my belief that prepl and nREPL are different enough that bolting prepl onto CIDER would mean papering over its limitations in dozens of subtle places. So instead of forcing it, that little experiment grew into two brand new projects of mine:
Both have write-ups of their own, so I won’t repeat the details here. I’m pretty excited about where they might lead - a great example of how digging into old technical debt can spark entirely new ideas.
This release is dedicated to São Miguel, the stunningly beautiful main island of the Azores archipelago.1 I got a lot of my recent inspiration for CIDER there, and naming the release after it felt right.
As always, none of this happens in a vacuum. A huge thank you to Alex Yakushev for his continued work on the inspector - it keeps getting better and better. And of course a massive shoutout to Clojurists Together and to all the other contributors and backers of my open-source work. You’re the reason CIDER and friends keep moving forward.
That’s all I have for you today. I hope you’ll enjoy using CIDER 1.22 as much as I enjoyed (eventually) shipping it. Keep hacking!
If you ever get the chance to visit, do it. Crater lakes, hot springs, and the greenest hills you’ve ever seen. ↩
Welcome to the Clojure Deref! This is a weekly link/news roundup for the Clojure ecosystem (feed: RSS).
Would you like to learn more about optimizing Clojure code? Anders Murphy investigated the performance of UUIDs as primary keys in SQLite. Also, Sashko Yakushev spent some time improving the performance of clojure-lsp. Learn from their experience, and if you’ve never had a chance, read about clj-async-profiler and differential flamegraphs. Useful tools!
And speaking of performance, FlowStorm 4.6.0 was
released. It added support for async and await in ClojureScript. Calling
FlowStorm a "debugger" only scratches the surface of what it can do. Take a
look at its ClojureScript support.
On the subject of Clojure dialects, Jolt, a new dialect hosted on Janet, has been making rapid progress since last week. It supports nREPL, Ring, deps.edn, Selmer, HoneySQL, and more. Join the #jolt channel on Clojurians Slack to keep up on all the latest developments. Jolt joins let-go, Glojure and Phel as Clojure dialects seeing a surge in development due to LLMs.
If you use Emacs, check out CIDER 1.22.0 which is out after 4 months of development work. Thanks to funding from Clojurists Together and others, Bozhidar was able to tackle some problems that have been bugging him for years! Perhaps they were also bugging you?
When you’re not editing Clojure application code, perhaps you’d like to edit some 3D shapes as Clojure code. For that, look no further than Ridley: a turtle graphics 3D modeling tool. Try it out online.
Clojure core can map, filter, and reduce millions of maps in memory, but should you? If your data situation has gotten to that point, you might want Flatiron, an in-memory columnar analytics library. Load your data and try some grouping, aggregating, filtering, and sorting—all in process, without a database.
Do you love immutable history but hate running out of storage? Use Event Store to save all your events in a minimal, append-only log in any S3-compatible object-store.
Business workflows and form logic may not be the most captivating technology in software engineering these days, but if you need to make a bunch of form-data screens, Stepvine is a new project to help. It is a "server-authoritative reactive form & app builder." You describe your forms as EDN data, and it runs all the logic on the server while keeping the UI responsive using Datastar.
You know what we say in the Clojure community: "the library isn’t dead, it’s just done." Well, sometimes it takes 7 years to discover it’s not done, and so, it was time to release again again, this time with circuit breakers. And if one day again, it’s time to release again, we look forward to seeing again again.
Save the date! HeartConf 2027 is happening August 18-19, 2027, in Mechelen, Belgium. It is the successor to Heart of Clojure, still with a strong Clojure representation, but also inviting friends from other communities.
Clojure/Conj 2026: Sep 30-Oct 2. Charlotte, NC, USA. Early Bird tickets.
EuroClojure 2027: May 19-21, 2027. Prague, Czech Republic. Join the mailing list
Deploying Ring Vs. Deploying Rails - A Rant. - Clojure Diary
Finding transitive var usages with clj-kondo - Michiel Borkent
Glint: Dev diary IV - One step forward, two steps back - Emil Bengtsson
Parediting in Helix - Tom Waddington
Navball MFD in sfsim - Jan Wedekind
Clojure is almost as fast as C (with some help) - Ertuğrul Çetin
Announcing ClojureSSH - Crispin Wellington
Deploying Ring App on Ubuntu 26.04 - Karthikeyan A K
biff.fx: lightweight effects system - Jacob O’Bryant
CIDER 1.22 (“São Miguel”) - Bozhidar Batsov
Making budget models punch above their weight with a smart harness - Dmitri Sotnikov
Debut release
flatiron - A columnar database for analytics
weowe - A native Android app (ClojureDart/Flutter) for tracking shared expenses between people. Local-first.
dvergr - FRP-based LLM agent framework with git-like memory model.
clj-colors - A color palette utility intended for generative art with clojure.
clojuressh - A Clojure library for using SSH in Clojure that is API compatible with bbssh
libl.in - A URL redirect app for all Codeberg users
eventstore - A minimal, production-oriented append-only event log, backed by S3-compatible object storage.
todo-sqlite-eventstore - A basic todo app that stores events in Tigris Data and uses SQLite for the projected todos read model. Events are the source of truth; the todos table is rebuilt from projections.
biff.fx - Turn your functions into pure state machines.
biff.config - A light Biff wrapper around Aero.
stepvine - form building platform
Updates
sci 0.13.52 - Configurable Clojure/Script interpreter suitable for scripting and Clojure DSLs
phel-lang 0.44.0 - A functional, Lisp-inspired language that compiles to PHP. Inspired by Clojure, Phel brings macros, persistent data structures, and expressive functional idioms to the PHP ecosystem.
flow-storm-debugger 4.6.0 - A debugger for Clojure and ClojureScript with some unique features.
bareforge 0.7.1 - Companion visual builder for BareDOM web components. Drag components, declare reactive state, export fully interactive CLJS or JS project
mount 0.1.24 - managing Clojure and ClojureScript app state since (reset)
quiescent 0.3.0 - A Clojure library for composable async tasks with automatic parallelization, structured concurrency, and parent-child and chain cancellation
promesa 12.0.0 - A promise library & concurrency toolkit for Clojure and ClojureScript.
teensyp 0.6.4 - A small, zero-dependency Clojure TCP server that uses Java NIO
yggdrasil 0.2.29 - Git-like, causal space-time lattice abstraction over systems supporting this memory model.
hawk 1.0.14 - It watches your code like a hawk! You like tests, right? Then run them with our state-of-the-art Clojure test runner.
spindel 0.1.23 - Cross-platform FRP runtime with a git-like memory model.
pg-datahike 0.1.45 - Postgres compatibility layer for Datahike.
ClojureScriptStorm 1.12.145-1 - A fork of the official ClojureScript compiler with extra code to make it a dev compiler
charred 1.039 - zero dependency efficient read/write of json and csv data.
ridley 3.1.0 - A turtle graphics-based 3D modeling tool for 3D printing. Write Clojure scripts, see real-time 3D preview, export STL. WebXR support for VR/AR visualization.
raster 0.1.3 - Fast, functional numerical computing for Clojure/JVM.
html 0.2.5 - Html generation library inspired by squint’s html tag
dataspex 2026.06.3 - See the shape of your data: point-and-click Clojure(Script) data browser
stratum 0.3.75 - Versioned, fast and scalable columnar database.
clj-tg-bot-api 1.2.270 - 🤖 The latest Telegram Bot API spec and client lib for Clojure-based apps
svar 0.7.29 - Type‑safe LLM output for Clojure. Works with any text‑only model.
ansatz 0.1.58 - Dependently typed Clojure DSL with a Lean4 compatible kernel.
cider 1.22.0 - The Clojure Interactive Development Environment that Rocks for Emacs
deft 0.2.0 - A collection of macros designed to address issues with objects in Clojure.

Translations: Russian
Syntax highlighting is a tool. It can help you read code faster. Find things quicker. Orient yourself in a large file.
Like any tool, it can be used correctly or incorrectly. Let’s see how to use syntax highlighting to help you work.
Most color themes have a unique bright color for literally everything: one for variables, another for language keywords, constants, punctuation, functions, classes, calls, comments, etc.
Sometimes it gets so bad one can’t see the base text color: everything is highlighted. What’s the base text color here?

The problem with that is, if everything is highlighted, nothing stands out. Your eye adapts and considers it a new norm: everything is bright and shiny, and instead of getting separated, it all blends together.
Here’s a quick test. Try to find the function definition here:

and here:

See what I mean?
So yeah, unfortunately, you can’t just highlight everything. You have to make decisions: what is more important, what is less. What should stand out, what shouldn’t.
Highlighting everything is like assigning “top priority” to every task in Linear. It only works if most of the tasks have lesser priorities.
If everything is highlighted, nothing is highlighted.
There are two main use-cases you want your color theme to address:
1 is a direct index lookup: color → type of thing.
2 is a reverse lookup: type of thing → color.
Truth is, most people don’t do these lookups at all. They might think they do, but in reality, they don’t.
Let me illustrate. Before:

After:

Can you see it? I misspelled return for retunr and its color switched from red to purple.
I can’t.
Here’s another test. Close your eyes (not yet! Finish this sentence first) and try to remember what color your color theme uses for class names?
Can you?
If the answer for both questions is “no”, then your color theme is not functional. It might give you comfort (as in—I feel safe. If it’s highlighted, it’s probably code) but you can’t use it as a tool. It doesn’t help you.
What’s the solution? Have an absolute minimum of colors. So little that they all fit in your head at once. For example, my color theme, Alabaster, only uses four:
That’s it! And I was able to type it all from memory, too. This minimalism allows me to actually do lookups: if I’m looking for a string, I know it will be green. If I’m looking at something yellow, I know it’s a comment.
Limit the number of different colors to what you can remember.
If you swap green and purple in my editor, it’ll be a catastrophe. If somebody swapped colors in yours, would you even notice?
Something there isn’t a lot of. Remember—we want highlights to stand out. That’s why I don’t highlight variables or function calls—they are everywhere, your code is probably 75% variable names and function calls.
I do highlight constants (numbers, strings). These are usually used more sparingly and often are reference points—a lot of logic paths start from constants.
Top-level definitions are another good idea. They give you an idea of a structure quickly.
Punctuation: it helps to separate names from syntax a little bit, and you care about names first, especially when quickly scanning code.
Please, please don’t highlight language keywords. class, function, if, elsestuff like this. You rarely look for them: “where’s that if” is a valid question, but you will be looking not at the if the keyword, but at the condition after it. The condition is the important, distinguishing part. The keyword is not.
Highlight names and constants. Grey out punctuation. Don’t highlight language keywords.
The tradition of using grey for comments comes from the times when people were paid by line. If you have something like

of course you would want to grey it out! This is bullshit text that doesn’t add anything and was written to be ignored.
But for good comments, the situation is opposite. Good comments ADD to the code. They explain something that couldn’t be expressed directly. They are important.

So here’s another controversial idea:
Comments should be highlighted, not hidden away.
Use bold colors, draw attention to them. Don’t shy away. If somebody took the time to tell you something, then you want to read it.
Another secret nobody is talking about is that there are two types of comments:
Most languages don’t distinguish between those, so there’s not much you can do syntax-wise. Sometimes there’s a convention (e.g. -- vs /* */ in SQL), then use it!
Here’s a real example from Clojure codebase that makes perfect use of two types of comments:
Disabled code is gray, explanation is bright yellowPer statistics, 70% of developers prefer dark themes. Being in the other 30%, that question always puzzled me. Why?
And I think I have an answer. Here’s a typical dark theme:

and here’s a light one:

On the latter one, colors are way less vibrant. Here, I picked them out for you:
Notice how many colors there are. No one can remember that many.This is because dark colors are in general less distinguishable and more muddy. Look at Hue scale as we move brightness down:

Basically, in the dark part of the spectrum, you just get fewer colors to play with. There’s no “dark yellow” or good-looking “dark teal”.
Nothing can be done here. There are no magic colors hiding somewhere that have both good contrast on a white background and look good at the same time. By choosing a light theme, you are dooming yourself to a very limited, bad-looking, barely distinguishable set of dark colors.
So it makes sense. Dark themes do look better. Or rather: light ones can’t look good. Science ¯\_(ツ)_/¯
But!
But.
There is one trick you can do, that I don’t see a lot of. Use background colors! Compare:

The first one has nice colors, but the contrast is too low: letters become hard to read.
The second one has good contrast, but you can barely see colors.
The last one has both: high contrast and clean, vibrant colors. Lighter colors are readable even on a white background since they fill a lot more area. Text is the same brightness as in the second example, yet it gives the impression of clearer color. It’s all upside, really.
UI designers know about this trick for a while, but I rarely see it applied in code editors:

If your editor supports choosing background color, give it a try. It might open light themes for you.
Don’t use. This goes into the same category as too many colors. It’s just another way to highlight something, and you don’t need too many, because you can’t highlight everything.
In theory, you might try to replace colors with typography. Would that work? I don’t know. I haven’t seen any examples.
Using italics and bold instead of colorsSome themes pay too much attention to be scientifically uniform. Like, all colors have the same exact lightness, and hues are distributed evenly on a circle.
This could be nice (to know if you have OCD), but in practice, it doesn’t work as well as it sounds:
OkLab l=0.7473 c=0.1253 h=0, 45, 90, 135, 180, 225, 270, 315The idea of highlighting is to make things stand out. If you make all colors the same lightness and chroma, they will look very similar to each other, and it’ll be hard to tell them apart.
Our eyes are way more sensitive to differences in lightness than in color, and we should use it, not try to negate it.
Let’s apply these principles step by step and see where it leads us. We start with the theme from the start of this post:

First, let’s remove highlighting from language keywords and re-introduce base text color:

Next, we remove color from variable usage:

and from function/method invocation:

The thinking is that your code is mostly references to variables and method invocation. If we highlight those, we’ll have to highlight more than 75% of your code.
Notice that we’ve kept variable declarations. These are not as ubiquitous and help you quickly answer a common question: where does thing thing come from?
Next, let’s tone down punctuation:

I prefer to dim it a little bit because it helps names stand out more. Names alone can give you the general idea of what’s going on, and the exact configuration of brackets is rarely equally important.
But you might roll with base color punctuation, too:

Okay, getting close. Let’s highlight comments:

We don’t use red here because you usually need it for squiggly lines and errors.
This is still one color too many, so I unify numbers and strings to both use green:

Finally, let’s rotate colors a bit. We want to respect nesting logic, so function declarations should be brighter (yellow) than variable declarations (blue).

Compare with what we started:

In my opinion, we got a much more workable color theme: it’s easier on the eyes and helps you find stuff faster.
I’ve been applying these principles for about 8 years now.
I call this theme Alabaster and I’ve built it a couple of times for the editors I used:
It’s also been ported to many other editors and terminals; the most complete list is probably here. If your editor is not on the list, try searching for it by name—it might be built-in already! I always wondered where these color themes come from, and now I became an author of one (and I still don’t know).
Feel free to use Alabaster as is or build your own theme using the principles outlined in the article—either is fine by me.
As for the principles themselves, they worked out fantastically for me. I’ve never wanted to go back, and just one look at any “traditional” color theme gives me a scare now.
I suspect that the only reason we don’t see more restrained color themes is that people never really thought about it. Well, this is your wake-up call. I hope this will inspire people to use color more deliberately and to change the default way we build and use color themes.
I have a weird relationship with statistics: on one hand, I try not to look at it too often. Maybe once or twice a year. It’s because analytics is not actionable: what difference does it make if a thousand people saw my article or ten thousand?
I mean, sure, you might try to guess people’s tastes and only write about what’s popular, but that will destroy your soul pretty quickly.
On the other hand, I feel nervous when something is not accounted for, recorded, or saved for future reference. I might not need it now, but what if ten years later I change my mind?
Seeing your readers also helps to know you are not writing into the void. So I really don’t need much, something very basic: the number of readers per day/per article, maybe, would be enough.
Final piece of the puzzle: I self-host my web projects, and I use an old-fashioned web server instead of delegating that task to Nginx.
Static sites are popular and for a good reason: they are fast, lightweight, and fulfil their function. I, on the other hand, might have an unfinished gestalt or two: I want to feel the full power of the computer when serving my web pages, to be able to do fun stuff that is beyond static pages. I need that freedom that comes with a full programming language at your disposal. I want to program my own web server (in Clojure, sorry everybody else).
All this led me on a quest for a statistics solution that would uniquely fit my needs. Google Analytics was out: bloated, not privacy-friendly, terrible UX, Google is evil, etc.
What is going on?Some other JS solution might’ve been possible, but still questionable: SaaS? Paid? Will they be around in 10 years? Self-host? Are their cookies GDPR-compliant? How to count RSS feeds?
Nginx has access logs, so I tried server-side statistics that feed off those (namely, Goatcounter). Easy to set up, but then I needed to create domains for them, manage accounts, monitor the process, and it wasn’t even performant enough on my server/request volume!
So I ended up building my own. You are welcome to join, if your constraints are similar to mine. This is how it looks:

It’s pretty basic, but does a few things that were important to me.
Extremely easy to set up. And I mean it as a feature.
Just add our middleware to your Ring stack and get everything automatically: collecting and reporting.
(def app
(-> routes
...
(ring.middleware.params/wrap-params)
(ring.middleware.cookies/wrap-cookies)
...
(clj-simple-stats.core/wrap-stats))) ;; <-- just add this
It’s zero setup in the best sense: nothing to configure, nothing to monitor, minimal dependency. It starts to work immediately and doesn’t ask anything from you, ever.
See, you already have your web server, why not reuse all the setup you did for it anyway?
We distinguish between request types. In my case, I am only interested in live people, so I count them separately from RSS feed requests, favicon requests, redirects, wrong URLs, and bots. Bots are particularly active these days. Gotta get that AI training data from somewhere.
RSS feeds are live people in a sense, so extra work was done to count them properly. Same reader requesting feed.xml 100 times in a day will only count as one request.
Hosted RSS readers often report user count in User-Agent, like this:
Feedly/1.0 (+http://www.feedly.com/fetcher.html; 457 subscribers; like FeedFetcher-Google)
Mozilla/5.0 (compatible; BazQux/2.4; +https://bazqux.com/fetcher; 6 subscribers)
Feedbin feed-id:1373711 - 142 subscribers
My personal respect and thank you to everybody on this list. I see you.

Visualization is important, and so is choosing the correct graph type. This is wrong:

Continuous line suggests interpolation. It reads like between 1 visit at 5am and 11 visits at 6am there were points with 2, 3, 5, 9 visits in between. Maybe 5.5 visits even! That is not the case.
This is how a semantically correct version of that graph should look:

Some attention was also paid to having reasonable labels on axes. You won’t see something like 117, 234, 10875. We always choose round numbers appropriate to the scale: 100, 200, 500, 1K etc.
Goes without saying that all graphs have the same vertical scale and syncrhonized horizontal scroll.
We don’t offer much (as I don’t need much), but you can narrow reports down by page, query, referrer, user agent, and any date slice.
It would be nice to have some insights into “What was this spike caused by?”
Some basic breakdown by country would be nice. I do have IP addresses (for what they are worth), but I need a way to package GeoIP into some reasonable size (under 1 Mb, preferably; some loss of resolution is okay).
Finally, one thing I am really interested in is “Who wrote about me?” I do have referrers, only question is how to separate signal from noise.
Performance. DuckDB is a sport: it compresses data and runs column queries, so storing extra columns per row doesn’t affect query performance. Still, each dashboard hit is a query across the entire database, which at this moment (~3 years of data) sits around 600 MiB. I definitely need to look into building some pre-calculated aggregates.
One day.
Head to github.com/tonsky/clj-simple-stats and follow the instructions:

Let me know what you think! Is it usable to you? What could be improved?
Once you have coded a Ring app,
$ lein ring uberjar
This will create a standalone JAR file in the target directory. Let’s renameit to my-app.jar for convenience.
Here’s how to host your Clojure Ring app on an Ubuntu VPS:
sudo apt update
sudo apt install openjdk-25-jre-headless
java -version # verify
From your local machine:
scp my-app.jar user@your-vps-ip:/home/user/
java -jar my-app.jar
Your app likely starts on port 3000 by default. You can override it:
PORT=8080 java -jar my-app.jar
You can use browser to test your app at http://your-vps-ip:3000, or http://your-vps-ip:8080 if you set a custom port.
Create a service file:
sudo nano /etc/systemd/system/my-app.service
Paste this:
[Unit]
Description=My Clojure Ring App
After=network.target
[Service]
User=user
WorkingDirectory=/home/user
ExecStart=/usr/bin/java -jar /home/user/my-app.jar
Restart=always
RestartSec=5
Environment=PORT=3000
[Install]
WantedBy=multi-user.target
Enable and start it:
sudo systemctl daemon-reload
sudo systemctl enable my-app
sudo systemctl start my-app
sudo systemctl status my-app
sudo apt install -y nginx
Create a config:
sudo nano /etc/nginx/sites-available/my-app
server {
listen 80;
server_name your-domain.com; # or your VPS IP
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Enable it:
sudo ln -s /etc/nginx/sites-available/my-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo ufw allow 'Nginx Full'
sudo ufw allow OpenSSH
sudo ufw enable
sudo snap install --classic certbot # install certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot # Prepare the Certbot command
sudo certbot --nginx # follow the prompts to set up HTTPS
Certbot will auto-edit your Nginx config and set up auto-renewal.
Useful commands after deployment:
sudo systemctl restart my-app # restart after uploading new jar
sudo journalctl -u my-app -f # tail logs
sudo systemctl status my-app # check status
When you deploy a new version, just scp the new jar over and sudo systemctl restart my-app.
I was recently asked for book recommendations and realized I have never written down the ones that actually changed how I think. Not the ones that taught me a language or a framework. The ones that shifted something in me as an engineer. Here they are, in no particular order.
Short, opinionated, and right about most things. The core idea is Deep Modules: hide complexity behind simple interfaces. This is ETC from another angle. If your module has a complex surface, every consumer pays that tax forever. Make the surface small and the internals can evolve freely. I read it in a weekend. I have been thinking about it for years.
I already thought in systems before reading this. What Meadows gave me was the vocabulary. Stocks and flows. Feedback loops. Leverage points. The places to intervene in a system, ranked from least to most effective. When I write about NixOS or Emacs or OneLoop, I am reaching for these ideas even when I do not name them directly. Short and elegant.
Hamming spent decades at Bell Labs studying what separates great scientists from merely good ones. His central question: why do so few people make significant contributions while so many are forgotten? The answer is mostly about how you allocate your attention and whether you have the courage to work on important problems instead of safe ones. This maps directly to what I wrote about transmittable vs. untransmittable knowledge. Hamming is rigorous, contrarian, and deeply practical.
The best systems engineering book of the last decade. Kleppmann builds from simple primitives (logs, hashes, SSTables) all the way up to distributed systems, always grounding abstractions in the physical realities they paper over. Even if you never build a distributed database, the way Kleppmann thinks will change how you design any system. This book is first principles applied to data.
Interviews with 19 legendary programmers from the 1980s. Bill Gates, Andy Hertzfeld, Charles Simonyi, and others. What makes it timeless is the focus on how they think about problems, not what they built. You start to see patterns across how these minds approach complexity.
I live the Unix philosophy every day. Reading it from the source is
different than absorbing it osmotically. Kernighan and Pike do not
just describe the philosophy. They demonstrate it, building real
tools from shell pipelines, showing how composability creates power
that no monolith can match. Every time I write curl | jq or compose
tools in Emacs, I am living this book. My OneLoop agent (read, write,
edit, bash as four primitives) is an example of Unix way of doing things.
The spiritual ancestor of everything I love about Lisps. Homoiconicity. Functions as first-class citizens. Building abstractions upward from the smallest possible primitives. SICP teaches you to think about computation itself, not any particular language. If you are a Clojure programmer and you have not read it cover to cover, fix that.
Not a software book, and that is the point. Norman's central idea is that good design is invisible and bad design blames the user. This applies to everything from Makefile help targets to agent tool interfaces. Your self-documenting system? Your one obvious path? Your inspectability obsession? Norman gives you the design vocabulary to articulate why something feels wrong and how to fix it.
Weizenbaum wrote ELIZA in 1966, then spent the rest of his career warning about delegating human judgment to machines. He is not anti-computer. He is pro-human. He argues that some things should not be computed, not because they cannot be, but because doing so corrupts the thinking. Reading this in 2026, in the middle of the AI hype cycle, feels like finding a kindred spirit from fifty years ago. This is the philosophical depth behind "the agent is not the point."
The original essay on why adding people to a late project makes it later. But it is really about something deeper: the irreducible complexity of communication and the necessity of conceptual integrity in software. Brooks' concept of conceptual integrity (a system should reflect one set of design ideas, not a committee's compromises) is my oneness principle by another name. The anniversary edition includes "No Silver Bullet," which is even more relevant in the age of "AI will solve everything."
—
These are not the only books I have read. They are the ones I keep coming back to. The ones where I will catch myself mid-conversation reaching for an idea and realizing it came from one of these pages. If you have read them, you know what I mean. If you have not, start with 1, 2, and 9.
Implementing REST APIs in Clojure, Praise for “Careless People”.
Here are the first updates for short term projects funded in Q2 2026. You can find overviews of these projects and the two others which will be reporting on a slightly different schedule in the original funding announcement. Thanks everyone!
Clojure LLM: Dragan Djuric
Gloat and Glojure: Ingy dot Net
Malli: Ambrose Bonnaire-Sergeant
Q2 2026 Report 1. Published May 31, 2026
The goal is to provide a high performance local LLM (large language model) AI solution, that supports mainstream open models, freely available at Hugging Face and elsewhere. Something like llama.cpp (https://llama-cpp.com/), but (hopefully!) simpler and faster, with both GPU and CPU support baked-in from the start.
I even have a catchy name for the library: iLLaManati :)
iLLaManati should:
In the first month, main focus was on the hammock, but I also accomplished plenty of implementation. I already written some elegant code for some parts and some code with very promising performance for the others. The main sauce, the LLM prefill/decode loop, and the KV-cache management, has not yet been written, and I am still running experiments and trying to conquer ONNX Runtime performance pitfalls.
I have researched how to build LLMs from scratch and identified relative performance-sensitive areas. This enabled me to better understand the current playing field, and be prepared to deal with various implementation libraries for running LLMs. It also enables me to understand where ONNX Runtime falls short when it comes to top of the line LLM performance. Whether it can be wrangled to be neck to neck with the fastest implementations is unlikely (although I hope it’s not impossible, so we’ll see), but it’s still a good choice as the first backend since it’s relatively approachable, and it runs on diverse hardware (+ we already have Diamond ONNX Runtime in Clojure). Anyway, I already see how it is going to at least direct the UX/API/architecture base for a polymorphic backend design of the future.
I have implemented an universal token encoder by integrating HuggingFace’s tokenizer via its Java wrapper.
It supports all major models (not explicitly tested yet).
For token decoding, I tried using HuggingFace’s tokenizer, but it’s API and implementation does
not fit with the planned UX, so I implemented my own streaming tokenizer which is
The initial version is available here: https://github.com/uncomplicate/iLLaManati/tree/main/illamanati-tokenizer
Dropped plans for sentencepiece integration, as our tokenizer is compatible with it.
This is the current highlight! During the hammock phase I got an idea of a novel (publication worthy!) fused sampler for the token decoding phase. It’s super fast
As I’ve already written, this part is an (early) WIP. I experimented a lot, wrote some code, that code gave me some information, and I’m grinding it bit by bit. So, I can be pretty sure we’ll have a pretty good implementation of LLM prefill and decode, and some nice KV-management for local models, but whether it will be fast enough with its ONNX Runtime engine to be put against the big boys depends more on the capabilities of ONNX Runtime itself, than my ability to code around it.
Even if I don’t manage to work around it’s deficiencies, we should at leastt get an okay engine during this funding cycle, but a great general base that could replace ORT directly with vendor libraries such as TensorRT and OpenVINO for achieving top speed in the future.If that happens to not be enough for us (we Clojurists want the best of the best :) I’ll try to make this base flexible enough to be able to integrate with the battleship solutions, such as TensorRT-LLM, directly, but that’s the story for another time :)
To accommodate the requirements of iLLaManati, I worked on assorted improvements and upgrades
in Uncomplicate libraries. I also spent a lot of time compiling upstream C++ code and dealing
with cryptic C++ compiler shenanigans, that I am constantly reminded why Clojure is so great to work with :)
Q2 2026 Report 1. Published June 16, 2026
We are halfway through the Q2 2026 Clojurists Together funding cycle, so this is a good time to report what has been done for Gloat and Glojure.
The grant officially started on May 1, but since I had learned that it was accepted a couple weeks earlier, I got too excited and decided to start working on it immediately. I was able to quickly get Glojure to pass the entire Clojure Compatibility Test Suite; except for 9 tests that are skipped as being not feasible. That gave Gloat a much stronger base to build on.
Since the start of the grant period, Gloat and Glojure have had over 20
releases, with Gloat moving from v0.1.26 to
v0.1.50.
The Glojure work was all being done on the long running fork
gloathub/glojure, but I’m thrilled to
announce that as of today, the work has been fully moved back to the upstream
glojurelang/glojure and will
continue to be maintained and released from there.
My overall ambition for Gloat is to have Clojure be as full featured and prominent to Go programming as it is to Java. The industry is crazy about Go. Let’s get it crazy about Clojure.
Furthermore, given that Go cross-compiles to almost everywhere, I am bullish on Gloat being a serious alternative to GraalVM’s native-image compiler. An alternative with fast compilation, expansive compilation targets (including WebAssembly) and completely open source licensing.
In the funding announcement post) I listed three main goals:
The CCTS goal landed early, just before the official grant start. That did not mean compatibility was finished, but it meant the core language was good enough to shift attention toward real-world use: interop, REPLs, dependencies, docs, examples, and the missing pieces that appear when you stop testing toy programs and start trying to use the system.
The short version: the first half of the grant has been mostly about making
Gloat usable and explainable.
The most important foundation work happened in April: the Gloat/Glojure stack got to the point where Glojure passed Jank’s Clojure compatibility test suite.
For Gloat, that matters because Gloat is not trying to invent a new language. The point is to take normal Clojure source, route it through Glojure and Go, and end up with Go source, native binaries, Wasm modules, or shared libraries. Every compatibility test that passes is one less place where user code can surprise us later.
After that milestone, I added a report page
to the Gloat site so the compatibility state is easier to see for yourself.
The most visible work this period was the REPL.
At the beginning of the cycle, Gloat was mostly an ahead-of-time compiler.
You pointed it at source code and got output. That is still the center of the project, but a serious language tool needs an
interactive story too. The Glojure glj binary already had a working CLI REPL, but I really wanted
to push Gloat’s CLI REPL beyond anything I’d seen to date (in any language).
gloat --repl) now starts a REPL CLI with:
I also added client modes so Gloat can connect to other Clojure-family REPLs.
For example, gloat --repl=+bb, gloat --repl=+clj, and
gloat --repl=+let-go all use the same general idea: install the requested
tool if needed, start it in repl server mode and connect with Gloat’s fancy UX.
You can also connect to any existing Clojure REPL with gloat --repl=12345 or
gloat --repl=.port-file.
This work was partly for Gloat itself and partly for comparison. When you are building a Clojure implementation, it is extremely useful to be able to jump between Clojure, Babashka, Glojure, let-go, and other dialects quickly.
I’m super proud of how far this CLI REPL has come so far.
I hope you take some time to kick the tires and take it for a test drive.
Let me know what you think!
The REPL work also produced a browser REPL.
The Gloat website now has a Glojure REPL page backed by a WebAssembly build of Glojure. It has syntax highlighting, multiline input handling, toolbar controls, rainbow brackets, and shareable sessions.
That matters for two reasons.
First, it makes the project easier to try. People can evaluate Glojure in the browser without installing Go, Glojure, Gloat, Java, or anything else.
Second, it proves out one of the promises of the stack: Clojure code can travel through Glojure and Go into Wasm. The demo is not just a toy on the side; it exercises the same direction the compiler is trying to make practical.
My favorite part is sharable stateful URLs. Try this one!
The biggest compatibility expansion was JVM-style interop.
Glojure is hosted on Go, not Java, but most real Clojure code assumes at least
some java.lang behavior exists. Even simple programs use things like Math/sqrt, System/getenv, strings, regexes, UUIDs, numeric wrappers, and time classes.
During this period I added docs, demos, and tests around a growing Go-native Java compatibility layer, gojava. The current Java class set is:
java.lang.Mathjava.lang.Systemjava.lang.Integerjava.lang.Longjava.lang.Stringjava.lang.Doublejava.lang.Booleanjava.lang.Characterjava.util.regex.Patternjava.util.UUIDjava.lang.Thread/sleepjava.time.InstantThe goal is not to make Glojure secretly become a JVM. The goal is to make normal Clojure source behave correctly when it uses common JVM interop forms.
For example, (Math/floorDiv -7 2) should return the JVM result, not the Go
integer-division result.
(System/getenv "PATH") should feel like Clojure.
Fully-qualified java.lang.Math/abs and bare Math/abs should both resolve in
the expected way.
Fresh namespaces should have host-class imports populated in a way that matches
what Clojure programmers expect.
This is the kind of work that makes unmodified Clojure code more likely to run.
gljdeps.ednThe other side of the interop story is Go.
Glojure already has Go interop, but Gloat needed clearer Go interop docs runnable demos, and an explicit way for Gloat programs to declare third-party Go packages.
I added a full Go interop guide covering:
I also added runnable demo files for each of those cases.
The more important practical change is gljdeps.edn
support in AOT compilation.
Gloat now resolves deps from --deps=, GLOAT_GLJDEPS, or a local
./gljdeps.edn, passes that file through to the Glojure compile workspace, and
injects the declared modules into the generated go.mod.
For example, a Glojure program can declare a dependency on a Go module, compile through Gloat, and end up with a generated Go module or native binary linked against that Go package. That moves Gloat closer to the original promise: Clojure syntax and semantics, with access to the Go ecosystem and Go’s build targets.
There is still more work needed here.
For public modules, AOT builds can sometimes succeed without gljdeps.edn
because go mod tidy can discover missing imports through the Go module proxy.
The deps file is still the reproducible path: it pins versions, supports
private/offline builds, and is required for REPL use where there is no final
go build discovery step.
Some dependency declaration and interop edges are still rough.
But the basic path exists now, and it has docs and demos behind it.
Another important milestone: Gloat has moved back to upstream Glojure.
At the end of May I met with James Hamlin, the original author and owner of Glojure. He gave me full permission to maintain the project.
That is a big deal for Gloat. Since the beginning of the Gloat project I had been working on a fork of Glojure, because I needed to make a lot of changes quickly: compiler changes, runtime changes, REPL work, Java compatibility, AOT support, packaging, and release mechanics. A fork was the practical way to move fast, but it was never the ideal long-term shape.
As of today, Glojure is unforked. Gloat is using and releasing from the original upstream Glojure repository again.
One of the important things I had added on the fork was prebuilt binaries for
Linux and macOS.
That made Gloat and Glojure (glj) installs much faster, because users did not
need to build the Glojure toolchain from source on first run.
That prebuilt-binary release flow now works upstream too with the just released
Glojure v0.6.5
version.
That is the first upstream Glojure release since
v0.6.4, which
came out last October.
It means the work is no longer living off to the side in a Gloat-specific fork.
It is going back into the main Glojure project where it belongs.
The surrounding Gloat work also tightened release/development workflow around
Glojure versions, local overrides, Go module replacement behavior, and local
worktrees.
Those details matter because Gloat, Glojure, gojava,
go-readline, and the
YAMLScript stdlib package all move together during
development.
I added a lot of documentation this period. New or heavily expanded docs include:
The generated man pages and website docs were refreshed too.
The demos matter as much as the prose. There are now runnable examples for Go interop, Java interop, and third-party
Go dependencies. Docs that only explain an idea are easy to get wrong. Docs that point to runnable files are much harder to fake.
One unexpected side project was Clojure.cc.
I built it because Gloat and Glojure make more sense when people can see the larger Clojure dialect landscape. The site catalogs Clojure-family languages and lets people compare hosts, status, release activity, tags, and project links.
It includes Gloat, Glojure, let-go, Babashka, ClojureScript, Jank, Basilisp, Phel, Fennel, Janet, and many others. It also has instant command launchers:
$(make -f <(curl -sL clojure.cc/cmd.mk) glj)
$(make -f <(curl -sL clojure.cc/cmd.mk) gloat) --repl
$(make -f <(curl -sL clojure.cc/cmd.mk) lg) -e '(apply + (range 3 10))'
This is not the core Gloat compiler work, but it supports the same goal:
make these dialects easier to discover, try, and compare.
It also gave me another place to exercise the Glojure browser REPL and the
multi-dialect REPL launcher work.
The REPL comparison work also pulled in let-go, another Go-hosted Clojure-like language.
Gloat can now launch/connect to let-go as one of the supported external REPL targets, and Clojure.cc includes it in both the dialect catalog and the instant launcher.
The more interesting part is that the two projects are starting to challenge and inspire each other. Gloat/Glojure has been pushing hard on AOT compilation, Go interop, and Clojure compatibility. let-go, meanwhile, has a very promising performance story. It compiles to bytecode today, and the let-go team is now exploring AOT compilation by turning that bytecode into Go code.
I have been talking with let-go’s author, Marcin Gasperowicz (@nooga), and with Norman Nunley, one of the main contributors working on performance. Norman and I knew each other about twenty years ago in the Perl world, so this is also a fun reconnection across language communities.
There is a real chance for collaboration here.
One direction I have been thinking about is making Gloat an abstraction over
both Glojure and let-go: same user-facing tool, potentially different Clojure-
on-Go engines underneath.
It is too early to promise that shape, but the overlap is obvious enough that
it is worth exploring seriously.
The original grant goal of smaller and faster binaries still needs more direct attention over the final 6 weeks of this grant.
Some groundwork is already there, especially the earlier -Xprune work for
dropping unused clojure.core code, but I have not spent enough of this cycle
yet on output size and runtime speed.
The first half pulled me toward compatibility, REPLs, dependencies, and docs,
because those were the things blocking real use and feedback.
For the second half, the main areas are:
gljdeps.edn and Go interop edgesThanks to Clojurists Together and everyone who funds it. This kind of work needs long stretches of focused time, and the funding makes that possible.
Thanks also to the people building and maintaining the surrounding Clojure dialect ecosystem. Gloat sits in that larger context, and the project is better when it can learn from Clojure, ClojureScript, Babashka, Glojure, let-go, Jank, Basilisp, and the rest of the family.
Halfway through the grant, Gloat is much easier to try, much better documented, more compatible with ordinary Clojure code, and more connected to the Go ecosystem than it was on May 1.
Now back to making the binaries smaller and faster.
Q2 2026 Report 1. Published June 9, 2026
In this project, I am tackling exponential growth related to Malli refs.
Thanks to Metabase and Clojurists Together for funding this project.
My work in progress so far is in #1284. The rest of this report documents some surprising discoveries about Malli’s current behavior that should give some context on the work so far.
As usual for optimizations around refs, there’s
surprisingly few lines of code involved and it’s quite difficult to explain (like the
essay in malli.generator explaining about 10 lines of ref optimization code).
I’m reminded of Dan Friedman’s C311 course at Indiana University Bloomington, which taught how to switch an interpreter
from lexical to dynamic scoping with similarly few mind-bending changes.
The first challenge was understanding why Malli schemas themselves exhibit exponential growth in certain cases.
In my pull request, the is-counting-times function checks how many times the ::counting schema is initialized. This helps validate Malli’s current behavior.
The first interesting discovery is that the simplest “pointer” ref parses the pointed child twice.
For example, ::counting is created twice here:
(is-counting-times [:schema {:registry {::first ::counting}} ::first] 2)
Furthermore, adding unreferenced entries to the registry increases the initialization count:
(is-counting-times [:schema {:registry {::s0 ::counting}} ::s0] 2)
(is-counting-times [:schema {:registry {::s0 ::counting ::s1 ::s0}} ::s0] 3)
(is-counting-times [:schema {:registry {::s0 ::counting ::s1 ::s0 ::s2 ::s1}} ::s0] 4)
However, changing which registry entry we refer to in the schema body in the does not change the initialization count:
(is-counting-times [:schema {:registry {::s0 ::counting ::s1 ::s0 ::s2 ::s1}} ::s0] 4)
(is-counting-times [:schema {:registry {::s0 ::counting ::s1 ::s0 ::s2 ::s1}} ::s1] 4)
(is-counting-times [:schema {:registry {::s0 ::counting ::s1 ::s0 ::s2 ::s1}} ::s2] 4)
This led me to look into the initialization of property registries, a very subtle part
of Malli that uses an obscure option called ::allow-invalid-refs. As best I can tell,
this option is a symptom of this implementation approach causing the unexpected copies.
For example, let’s step through why the following schema creates 3 copies of ::counting:
[:schema {:registry {::s0 ::counting ::s1 ::s0}} ::s0]
The property registry parses ::s0 and ::s1
independently, effectively creating two disjoint “pointer” schemas. Then the body
triggers a third copy. Using defs this looks like:
(def s0 (m/-pointer ::s0 ::counting nil))
(def s1 (m/-pointer ::s1 (m/-pointer ::s0 ::counting nil) nil))
(def body (m/-pointer ::s0 ::counting nil))
Ideally, instead they would share an instance of ::counting:
(def s0 (m/-pointer ::s0 ::counting nil))
(def s1 (m/-pointer ::s1 s0 nil))
(def body s0)
The subtlety preventing this optimization is Malli’s dynamic scoping for refs. The current implementation intentionally treats each pointer to a property registry independently because the scope at that point is determined dynamically. This matters only in very rare, obscure, and (in my experience) often buggy circumstances, an insight that we can take advantage to optimize most schemas.
Here’s the simplest example I’m aware of that requires the most general implementation,
one where ::counting is never actually used, yet created twice:
(is-counting-times [:schema {:registry {::s0 ::counting ::s1 ::s0}}
[:schema {:registry {::s0 :int}}
::s1]]
2)
The inner registry overrides ::s1’s value for ::s0.
This is relevant because we have two different schemas called ::s1,
so naively reusing the same schema for both positions that mention ::s1
would be incorrect.
Hopefully these observations help understand some of the approaches in the PR.
In an earlier iteration of the PR, I managed to remove all exponential growth in schemas with non-overlapping registries, including the massive schema in the PR description which optimizes the parsed representation from including millions of copies of a single schema to just one canonical representation.
Next, I would like to tackle the exponential growth of generating a validator for this
schema. I initially attempted to solve the problems of exponential growth of schemas themselves and their validators separately, however I suspect there is a unifying solution for both.
That will hopefully be the next milestone.