Notify yourself when a task finishes

If you&aposve got a ridiculously good memory and you&aposve been reading my writing for a while, you know I&aposm a fan of processes notifying you when they are done. I often have some task running in a hidden terminal that performs actions when files change. This is most often running Clojure tests whenever a file changes using either test-refresh or lein-autoexpect. Another common watch task is rendering this website whenever one of the markdown files changes.

I don&apost like needing to have these processes always visible, since I mostly only care about when they finish. When running unit tests, I don&apost need to see the output unless a test is failing. When writing articles, I only care about when the rendering is done so I know I can refresh my browser to review the output.

On macOS, one way of doing this is using terminal-notifier. terminal-notifier makes it trivial to send notifications.

Below is the script I run while working on this website. It uses entr to monitor the input files; when changes are detected, it renders this site using my homegrown Babashka static site generator, and when that finishes, it uses terminal-notifier to alert me.

#!/bin/bash
while sleep 0.5; do 
    rg bb templates source --files -t css -t clojure -t markdown -t html \
        | entr -d -s &aposrm -rf output/*; bb render && terminal-notifier -message "Rendering complete"'
done

This site renders quickly, so I can usually make some edits, save, and toggle to a browser to refresh and see the output. Still, it is nice to see that little notification pop-up on my screen so I know for sure that if I hit refresh, I&aposm seeing the latest render.

When I&aposm running my Clojure tests, both lein-autoexpect and test-refresh send a notification with a pass or fail message based on the status of the unit tests that just ran. If the tests are passing, I don&apost have to glance at my terminal. If they are failing, I do.

I&aposd encourage you to think about what processes you might want to get notifications from when they are done and look into how to set that up. terminal-notifier works great on macOS. I can&apost make recommendations for other operating systems since it has been years since I&aposve used any alternatives besides SSHing into a Linux server.

It is worth the effort to figure out how to have notifications. They remove a trivial inconvenience (having to switch programs, needing to keep a window visible on your screen) and make life a little better. By stacking small, slightly life-improving techniques, all of a sudden you find yourself much more productive.

Permalink

Software Engineer at Scarlet

Software Engineer at Scarlet

gbp60000 - gbp110000

Our mission is to hasten the transition to universally accessible healthcare. We deliver on this mission by enabling innovators to bring cutting-edge software and AI to the healthcare market safely and quickly. We're regulated by the UK Government and European Commission to do so.

Our certification process is optimised for software and AI, facilitating a more efficient time to market, and the frequent releases needed to build great software. This ensures patients safely get the most up-to-date versions of life-changing technology.

Come help us bring the next generation of healthcare to the people who need it.

Our challenges

Product and engineering challenges go hand in hand at Scarlet. We know our mission can only be accomplished if we:

  • Build products and services that our customers love.
  • Streamline and accelerate complex regulatory processes without sacrificing quality.
  • Ensure that we always conduct excellent safety assessments of our customers’ products.
  • Continuously ship great functionality at a high cadence - and have fun while doing it.
  • Build and maintain a tech stack that embraces the complex domain in which we work.

Our engineering problems are plenty and we have chosen Clojure as the tool to solve them.

The team

The team is everything at Scarlet and we aspire to shape and nurture a team where every team member:

  • Really cares about our customers.
  • Works cross-functionally with engineers, product managers, designers, regulatory experts, and other stakeholders.
  • Collaborates on solving hard, important, real-world problems.
  • Helps bring out the best in each other and support each other’s growth.
  • Brings a diverse set of experiences, backgrounds, and perspectives.
  • Feels that what we do day-to-day is deeply meaningful.

We all have our fair share of experience working with startups, open source and various problem spaces. We wish to expand the team with ~2 more team members that can balance our strengths and weaknesses and help Scarlet build fantastic products.

We’re looking for ambitious teammates who have at least a few years of experience, have an insatiable hunger to learn, and want to do the most important work of their career!

How we work

Our ways of working are guided by a desire to perform at the highest level and do great work.

  • Flexible working: Remote-first with no fixed hours or vacation tracking.
  • Low/no scheduled meetings: Keep meetings to a minimum—no daily stand-ups or agile ceremonies.
  • Asynchronous collaboration: Have rich async discussions and flexible 1:1s as needed.
  • High trust and autonomy: Everyone solves problems; we are responsible for our choices and communicating them with our teammates.
  • Getting together: We meet a minimum of twice a year for a week at our offices in London.
  • Pick your tools: We believe in engineering excellence trust you to use the tool set you feel most productive with.

About you

If this sounds exciting to you, we believe Scarlet may be a great fit and would love to hear from you!

We believe that the potential for a great fit is even higher if you have one or more of the following:

  • Professional Clojure experience.
  • Professional full-stack web development experience.
  • Previous experience in the health tech / regulatory space
  • Endless curiosity and are always driven to understand why things are the way they are.
  • Superb written and verbal communication
  • Live within +/- 2h of the UK’s timezone

The interview process

Though the order may change, the interview steps are:

  1. Intro call with Niclas - 45 mins
  2. Technical knowledge call with an engineer - 60 mins
  3. Culture fit call with a product manager - 60 mins
  4. Technical workshop with Niclas - 90 mins
    • A pair programming session or a presentation of something you’ve built, the choice is yours
  5. Culture fit calls with our co-founders Jamie and James - 30 mins each
  6. Referencing & offer

We want your experience with Scarlet to be a good one and we do our utmost to ensure that you feel welcomed throughout the interview process.

Permalink

Plan for Clojure AI, ML, and high-performance Uncomplicate ecosystem in 2026

I've applied for Clojurists Together yearly funding in 2026. Here's my application. If you are a Clojurists Together member, and would like to see continued development in this area, your vote can help me keep working on this :)

My goal with this funding in 2026 is to continuously develop Clojure AI, ML, and high-performance ecosystem of Uncomplicate libraries (Neanderhal and many more), on Nvidia GPUs, Apple Silicon, and traditional PC. In this year, I will also focus on writing tutorals on my blog and creating websites for the projects involved, which is something that I wanted for years, but didn't have time to do because I spent all time on programming.

How that work will benefit the Clojure community

This will highly benefit the Clojure community as this is THE AI ecosystem for Clojure, and supporting AI is arguably the main focus on probably all software platforms. Clojure has something to offer on that front, beyond just calling OpenAI API as a web service!

Uncomplicate grew to quite a few libraries (of which some are quite big; just Neanderthal is 28,000 lines of highly-condensed, aggresively macroized, and reusable code): Diamond ONNX Runtime, Neanderthal, Deep Diamond, ClojureCUDA, ClojureCPP, Apple Presets, ClojureCL, Fluokitten, Bayadera, Clojure Sound, and Commons.

Here's a word or two of how I hope to improve each of these libraries with Clojurists Together funding in 2026.

Neanderthal (Clojure's alternative to NumPy, on steroids)

In 2025, Neanderthal celebrated its 10th birthday. It started as a humble but fast matrix and vector library for Clojure, but after 10 years of relentless improvements, now it boasts a general matrix/vector/linear algebra API implemented by no less than 5(!) engines for CPUs, GPU (Nvidia CUDA), GPU (OpenCL: AMD, Intel, Nvidia), Apple Silicon (Accelerate), and general CPU (OpenBLAS). And this is not a superficial support for the sake of ticking a check box; each of these engines support much more operations on exotic structures, and configuration options, than I've seen elsewhere. It has almost everything, but it doesn't (YET!) have a METAL-based engine for Apple GPUs. Let's work on that!

Deep Diamond (the Clojure Tensor and Deep Learning library, not quite unlike PyTorch, but of different philosophy)

In 2019, I started Deep Diamond as a demo showcase for Neanderthal's capabilities as the foundation for high-performance number-crunching software. It quickly outgrew that, and became a general Tensor/Deep Learning API, implemented by several fast, native optimized, backends, that run on both CPUs and GPUs, across hardware platforms (x86_64, GPUs, arm64, Apple Silicon, you name it) and operating systems (Linux, MasOS, Windows). Of course, it does not clash with Neanderthal, but complements it, in the best manner of highly focused Clojure libraries that do one job and do it well.

Deep Diamond is quite capable, but it cries for a METAL-based engine for Apple GPUs, too.

Diamond ONNX Runtime (the Clojure library for executing AI models)

This is the latest gem in Uncomplicate's store, and I developed it thanks to Clojurists Together funding in Q3 2025. Similarly to how I started Deep Diamond mainly as a teaching showcase for Neanderthal, I started this to show Clojure programmers how close we, Clojurists, are to the latest and shiniest AI stuff that everyone's raving about. But of course, being close does not mean that we can close the gap to the multi-billion funded Python ecosystem in a few afternoons. It needs laser-focused development and knowing what to do, when, and where. Nevertheless, Clojure is there. Now we can run inference on the trained models from Hugging Face and other vibrant AI communities directly in Clojure's REPL. Does this make an effortless billion-dollar AI startup? NO. Does it bring Clojurians to the party? YES! And there's more to come.

Not only that this library is new, but the whole wider ecosystem exploded in the last year with the wide availability of open-weights model that you can run at home. So, lots of functionality is added upstream all the time, and I hope to be able to stay current and have the best and newest stuff in Clojure..

ClojureCUDA (REPL-based low-level CUDA development)

Not many Clojurians may prefer to work with GPU directly, or to write their own kernels. Neither do I. But, this library is one of the un-celebrated workhorses that enables me to implement whatever I want in Neanderthal, Deep Diamond, and Diamond ONNX Runtime, instead of just trying to wrap whatever there is in upstream C++ libraies. ClojureCUDA gives us the superpower of choice: wrap whatever works, but then implement the missing parts yourself!

As CUDA is receiving a steady stream of changes and improvements, I'd like to improve and extend ClojureCUDA to always be in top shape! It is not as easy as it seems to the casual onlooker.

ClojureCPP (the gateway to native C++ libraries)

From 20,000 feet, integrating a native library to JVM and Clojure may look straightforward. Oh, how wrong they are. Virtually every C++ library is a special kind of jungle, with its own structures, patterns and inventions. What might seems a minor technical detail might require special acrobatics to support it on the JVM. Masking that mess under the hood so that a Clojurian do not need to care might be insanely brittle if it weren't for ClojureCPP! It is not as large as Neanderthal or Deep Diamond, but it is one of the reasons that enables these upper level libraries stay on the 25,000 or 3,000 lines of code mark, instead of being 500,000 or 50,000, as many of their counterparts in other languages.

Apple Presets (native JNI bindings for various Apple libraries)

Yup. To support Apple Silicon in Neanderthal and Deep Diamond I had to make these bindings, since there weren't any to "just" wrap. And to support more Apple high performance computing apis, I'll have to create additional bindings (for example, for METAL) and only then develop the Clojure part.

Fluokitten, Bayadera, ClojureCL, Commons, Clojure Sound, etc.

These libraries will not be in focus in 2026., but will probably need some care and assorted improvements here and there.

Summary:

In short, my focus with this funding with Clojurists Together will have two main branches:

  1. Development of new functionalities, supporting more hardware and platforms for existing functionality, and fixing issues for a dozen Uncomplicate libraries that already exist. This is what is described in the text you've just read.
  2. Develop an unified website for Uncomplicate and stuff it with useful information in one place. Currently, some libraries have websites that I wrote many years ago, while some rely on GitHub Clojure tests, in-code documentation, tutorials on dragan.rocks and my books. There are many resources, some of which are quite detailed (2 full books!), but people without experience (which is the majority of Clojure programmers) have a hard time using all these in organized way. I hope to solve this with the unified website!

Permalink

Shipping little apps anywhere, anytime

I&aposm a big fan of making small (sometimes silly) programs. As a software developer, you have a superpower: you can identify problems in your life and fix them by creating some specific software that solves for exactly what you need. When scoped small enough, creating these tiny programs takes minimal time investment.

When you develop the practice of recognizing when a bit of software would be helpful, you see opportunities all the time. But you don&apost control when you get inspiration for these programs. So you come up with strategies for handling these bursts of inspiration.

One strategy: Write yourself a note (paper, email to yourself, some app on your phone) and maybe get around to it later. (You occasionally manage to get around to it later.) Another strategy: Think about the inspiration and trick yourself into thinking you&aposll remember it later when you&aposre at a computer. You justify this by claiming if you forget it, it must not have been important.

These workflows are fine but they leave a lot of room for never following up. With modern AI tools, we can do better.

My new strategy:

  1. Inspiration strikes!
  2. I pull out my phone and open my web browser to OpenAI&aposs Codex web app.
  3. I translate my inspiration into a task and type (or voice-to-text) it into Codex.
  4. I submit the task to Codex, go about my day, and check on it later.
  5. Later: read the diff, click the Codex button to open a PR, merge the PR through GitHub&aposs mobile interface, and let GitHub Actions deploy the changes to GitHub Pages.

I started using this technique in early summer 2025. Since then, I&aposve been able to develop and iterate on a handful of single-page web applications this way. As models improve, it is getting even easier to knock them out. It works well for either making a new application or tweaking an existing one.

Here is my setup:

  • I have a single repo named experiments1 on GitHub.
  • This repo has a subdirectory per application.
  • The applications are in a variety of web languages (HTML, CSS, TypeScript, JavaScript, ClojureScript).
  • OpenAI Codex is linked with this experiments repo.

With this setup, I&aposm able to follow the above strategy with minimal friction. If I have an idea for a new little application, I open Codex and provide a description of what I want and what it should be called, and it usually manages to start work on it. When I have an idea for tweaking an application, I open Codex, tell it what subdirectory the app is in and what tweak I want made. All of this can be done from a smartphone.

When Codex is done, I do a quick scan through the diff, click the buttons to open a PR, merge it, wait for the deploy, and then check on the deployed artifacts. The apps end up published at jake.in/experiments.

It isn&apost all smooth; sometimes a problem is introduced. Depending on the problem, I&aposll either revert the code and try again or give Codex more instructions and try to have it fix it. If really needed, I&aposll fire up my laptop and fix it myself or iterate with AI on fixing the problem there.

The bar has been seriously lowered for creating specific software. Go do it. It is fun, but in a different way than traditional programming.

  1. I don&apost know if this limitation still exists, but when I was initially setting this up my experiments repo had zero commits. This caused problems in Codex that were fixed by adding a single commit.

Permalink

Episode 11. Clojure still gives the biggest performance boost, with Jeremiah Via, NYT

In the 11th episode of "Clojure in product. Would you do it again?", Artem Barmin and Vadym Kostiuk speak with Jeremiah Via, Staff Software Engineer at The New York Times. Jeremiah describes how Clojure was introduced and adopted across the search stack at a major media organization, and why JVM interop and practical tooling made it the right choice for their data-processing workloads.

Our conversation walks through concrete topics: Jeremiah’s Clojure origin story, the iterative migration from PHP, Erlang, Python, and Java to JVM/Clojure services, and the search team’s day-to-day work, including how they push vector embeddings into Elasticsearch for AI features and performance.

We also dig into hiring and engineering practices: onboarding newcomers with an emphasis on functional thinking and REPL workflows, hiring for search/domain expertise over prior Clojure experience, maintaining code discipline, and addressing production concerns like memory sizing and performance tuning. As Jeremiah notes, "Now with AI stuff, people can be productive very fast without understanding it, using a cursor and tools like that," and he cautions that it remains to be seen how this will affect the deeper mental model of learning to think in Clojure.

We conclude with Jeremiah's response to a question from our previous guest, Cam Saul from Metabase.

Listen to our podcast and get more insights about Clojure in product: https://www.freshcodeit.com/podcast

Permalink

Clojars Maintenance & Support: August-October Update

2025 Critical Infrastructure: Clojars Maintentance and Support Update by Toby Crawley

August-October, 2025. Published November 22, 2025

This is an update on the work I’ve done maintaining Clojars with the support of Clojurists Together in August through October of 2025. Most of my work on Clojars is reactive, based on issues reported through the community or noticed through monitoring.
If you have any issues or questions about Clojars, you can find me in the #clojars channel on the Clojurians Slack, or you can file an issue on the main Clojars GitHub repository.

You can see the CHANGELOG for notable changes, and see all commits in the clojars-web and infrastructure repositories for this period. I also track my work over the years for Clojurists Together (and, before that, the Software Freedom Conservancy.

Below are some highlights for work done in August through October:

Though after October, I enjoyed the opportunity to chat with Clojars users at Clojure/Conj in Charlotte, NC! It was great to see old friends and make some new ones!

You can find earlier updates re Clojar fixes and updates here: March - July 2025

Permalink

Clojars Maintenance & Support: August-October 2025 Update

2025 Critical Infrastructure: Clojars Maintentance and Support Update by Toby Crawley

August-October, 2025. Published November 22, 2025

This is an update on the work I’ve done maintaining Clojars with the support of Clojurists Together in August through October of 2025. Most of my work on Clojars is reactive, based on issues reported through the community or noticed through monitoring.
If you have any issues or questions about Clojars, you can find me in the #clojars channel on the Clojurians Slack, or you can file an issue on the main Clojars GitHub repository.

You can see the CHANGELOG for notable changes, and see all commits in the clojars-web and infrastructure repositories for this period. I also track my work over the years for Clojurists Together (and, before that, the Software Freedom Conservancy.

Below are some highlights for work done in August through October:

Though after October, I enjoyed the opportunity to chat with Clojars users at Clojure/Conj in Charlotte, NC! It was great to see old friends and make some new ones!

You can find earlier updates re Clojar fixes and updates here: March - July 2025

Permalink

I am sorry, but everyone is getting syntax highlighting wrong

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.

Christmas Lights Diarrhea

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.

Enough colors to remember

There are two main use-cases you want your color theme to address:

  1. Look at something and tell what it is by its color (you can tell by reading text, yes, but why do you need syntax highlighting then?)
  2. Search for something. You want to know what to look for (which color).

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:

  • Green for strings
  • Purple for constants
  • Yellow for comments
  • Light blue for top-level definitions

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?

What should you highlight?

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.

Comments are important

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.

Two types of comments

Another secret nobody is talking about is that there are two types of comments:

  1. Explanations
  2. Disabled code

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 yellow

Light or dark?

Per 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.

Bold and italics

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 colors

Myth of number-based perfection

Some 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, 315

The 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 design a color theme together

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.

Shameless plug time

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.

Permalink

Clojure South conference at Nubank: two days of community, code, and collaboration

A little over a month ago, Nubank’s office in Vila Leopoldina, São Paulo became the meeting point for the Clojure community in South America. The second edition of the Clojure South conference gathered over 200 professionals, students, and language enthusiasts for two days of knowledge sharing, connection, and a shared passion for functional programming. 

It was a celebration of a community that grows collaboratively and that, within Nubank, has found not only an active and vibrant space, but also a real-world showcase of how functional programming, simplicity, and engineering excellence guide the way we build technology every day.

Why events like this matter

Clojure is a language built around collaboration, curiosity, and the desire to rethink how we develop software, and these same principles shape the spirit of Clojure South. By hosting and fully supporting the event, Nubank reinforces its commitment to strengthening the Clojure ecosystem not only internally but across South America.

Bringing together people from different countries, backgrounds, and experience levels creates a virtuous cycle: more learning, more shared practices, and more real solutions emerging from technology. It’s an essential step toward consolidating the region as a global reference for the language.

Highlights from the talks

Over two days, the conference offered a rich program: the first dedicated to hands-on workshops on Clojure and Datomic, and the second featuring talks by national and international guests, with highlights including several Nubankers, such as Alex Miller and Alessandra Sierra, Principal Software Engineers, Raíssa Barreira and João Lanjoni, Software Engineers.

12 Years of Component, with Alessandra Sierra

Alessandra revisited the journey of the Component library, created 12 years ago. With just over 200 lines of code, it profoundly influenced how Clojure systems are structured, inspiring extensions, variations, and even ports to other languages.

The talk connected history, impact, and reflections on how much the library achieved its original goals and where it can still evolve.

AI and incremental testing in Clojure, with Raíssa Barreira

Based on her master’s research at USP, Raíssa presented an incremental strategy for generating unit tests with the help of AI.

She showcased the main challenges, the tools involved, and how bridging academia and industry can improve software quality in large-scale systems.

NuFileBox Reverse: secure file management with Clojure, with Eric Bispo and Isaac Borges

Eric and Isaac, from the NuFuturo team (a partnership between Nubank, UFBA, and IFBA), showed how Clojure became the “brain” of a security architecture for file uploads and analysis.

NuFileBox Reverse orchestrates encryption, multiple programming languages, and real-time malware analysis with YARA, a concrete example of Clojure solving critical security challenges.

Joy of Data, with Alex Miller

Alex offered a different perspective on how we deal with data in everyday development. Instead of fragile structures, boilerplate, and frustration, Clojure treats data as simple values that can be transformed and composed.

He demonstrated how this mindset not only simplifies software but also makes working with data more enjoyable.

Elegant interfaces with ClojureScript, React, and UIx, with João Lanjoni

João explored how to build modern front-ends using ClojureScript, React, and the UIx library. He discussed the limitations of older approaches like Reagent and how UIx offers a more idiomatic and integrated layer for building elegant interfaces using the latest React features, all within ClojureScript.

Beyond the code

Between workshops, talks and coffee breaks, conversations ranged from macros, the Datomic database, to career paths, from distributed architecture to how we can train the next generation of Clojure developers in Brazil, reflecting themes that marked the sessions, such as the future of the Clojure ecosystem, real-world applications at scale and the evolution of the language in the industry. For those who were there, one thing became clear: events like this build community, connect generations, and help keep technology alive.

The second edition of Clojure South proved that there is a strong, curious, and committed community shaping the future of the language in South America. Nubank is proud to be part of this story and to support initiatives that strengthen the ecosystem.

And if there was one sentence that captured the spirit of the event, it was this one, heard in the hallways:

“Clojure isn’t just about parentheses. It’s about people building things together.”

The post Clojure South conference at Nubank: two days of community, code, and collaboration appeared first on Building Nubank.

Permalink

3 Lessons from implementing Controlled-Experiment Using Pre-Experiment Data (CUPED) at Nubank

Authors: The Nubank Experimentation Platform’s Data Science team is Abel Borges, Brando Morais, Luís Assunção, Paulo Rossi, Ramon Vilarino (in alphabetical order).
No feature in the platform would be possible without the support of the entire squad — a special thanks to Thiago Nunes (PM), Thiago Parreiras (Product lead), and Miguel Bitarello (Engineering lead).


Introduction

A/B testing is a cornerstone of product development at Nubank, allowing us to make data-informed decisions by comparing different versions of a product or feature to see which performs better. This rigorous approach ensures that every change we implement is backed by empirical evidence, leading to continuous improvement and a superior user experience.

However, a persistent challenge in experimentation is the issue of experiment sensitivity. Often, the effects we aim to measure are subtle or small, making them difficult to detect with statistical significance. This limitation can hinder our ability to identify changes and optimize our products effectively. To overcome this, our team embarked on a journey to explore advanced variance reduction techniques to improve statistical power.

Improving statistical power translates directly into increased precision for our estimates, thus reducing the duration of experiments. This allows for faster and safer iteration.

Among the approaches we investigated, “Controlled-Experiment using Pre-Experiment Data” (CUPED) stood out. CUPED is a statistical method that leverages pre-experiment data to reduce the variability of key metrics. By accounting for baseline differences among experimental units, CUPED can significantly improve the statistical power of our experiments, making it easier to detect even smaller, yet meaningful, effects.

This article shares the 3 key lessons we learned from the challenging yet rewarding process of implementing CUPED within Nubank’s robust experimentation platform. These insights offer valuable guidance for any organization looking to enhance precision and efficiency when scaling their A/B testing efforts.

Background 

CUPED was first introduced by Microsoft researchers Deng and others (2013) and has been widely used in companies such as Netflix, Booking, Eppo, Walmart, and many others. Notes by other researchers (Lin (2013), Deng and Hu (2015), Deng and others (2023)) highlight the relationship of CUPED to linear regression, and how different assumptions in CUPED map to different regression specifications.

Suppose that our experiment has a target metric Y which is calculated using all events for a given customer after they are exposed to the experiment. We’re interested in estimating the Average Treatment Effect (ATE), so we calculate the difference in means between treatment and control,

where Δ is an unbiased estimator of the ATE. Assume we’re able to compute a covariate X using events that occurred prior to the customer’s exposure to the experiment. With that, we can write

where θ is a scalar parameter. Since subjects were randomly assigned between control and treatment groups, E(Xt) -E(Xc)= 0 and thus , Δadjusted is also an unbiased estimator of the ATE.

The goal is to choose a value for θ that reduces the variance of Δadjusted result as much as possible. Using the expression for the variance of the linear combination of two random variables, we can expand the variance for Δadjusted:

By setting its first derivative to zero, we can determine the optimal value, θ*, that minimizes this variance:

And finally we have:

A natural choice for X are pre-exposure measurements of Y; for instance, key transactional metrics for a bank usually present strong serial correlation (i.e. correlation of the metric with itself in the past). However, this demonstration applies to any covariate.

Notice that if we write θ* in terms of the covariances between unit observations rather than between averages, then we can write:

where θc=Cov(Xc,,Yc) / Var(Xc)​​ and θt=Cov(Xt,,Yt)/Var(Xt)​​. In other words, it is a weighted average from two θ parameters of each group in terms of their sample variances.

Note that  θc and θt are coefficients of a simple linear regressions per group, and θ* is a combination of both. Further details can be found in Lin, Section 2 (or 3).

Lesson 1: Be aware of unequal variants.

When control and treatment groups are equally sized, θ* can be approximated from pooled statistics, as described in Deng et al. (2013):

which yields Δpooled. However when the experiment groups have different sample sizes (nc π nt) and the treatment changes the serial correlation of the metric (Cov(Xc,Yc) ≠ Cov(Xt,Yt) ), the variance of Δpooled can be larger than the variance of the naive estimator Δ.

When simulating scenarios with highly unequal groups (90%/10% split) and a treatment group’s covariance three times larger than control’s, the ATE distributions demonstrate reduced efficiency with a pooled estimation, as illustrated in the plot below.

At Nubank, we had originally implemented CUPED by estimating θpooledfor each variant pair for every experiment. However, we run multiple experiments that violate the assumptions of this original estimator.  Lin (2013) showed that *Δadjusted is at least as efficient as both Δ and Δ pooled. Following their conclusions, we assessed whether this change would be meaningful for our metrics and decided to change our implementation to use  θ*. Though seemingly minor, this change significantly enhanced the robustness and precision of our analyses, leading to more dependable experimental conclusions.

Lesson 2: How We Defined a Standard 42-Day Lookback Window

Following the decomposition of θ* as a weighted sum of θ-like coefficients within each group, the variance of  Δ*adjusted can be recast as a function of the metric standard deviations and serial correlations (pt, pc) between its post and pre-exposure versions,
π ∈ (0,1) being the sampling rate of the treatment group:

Note that the higher the correlation between post-exposure and pre-exposure metrics in absolute terms, the higher the variance reduction. As noted in the background section, a straightforward method is to set X equal to Y in the past, which naturally leads to using the same variable as the control variate during the pre-experiment observation window. However, this raises the question: How can we determine the optimal pre-experiment metric aggregation period for CUPED?

Nubank offers a diverse portfolio of products and services, ranging from its foundational credit card offerings to its expanding shopping marketplace. Given this inherent diversity, each product operates within its own distinct market and targets a different segment to the customer base. For example, the seasonality of a user searching for flights in our marketplace is very different from those investing in crypto assets.

Ideally, each metric and experiment would have its own pre-experiment window. However, this level of flexibility and implementation overhead is not currently feasible in our platform. Therefore, we chose a generic window that would adequately capture most past user behavior without significantly increasing pipeline costs.

For that, we ran several simulations, sampling subjects from our experiments with varying sample sizes and durations. This approach allowed us to incorporate simulated data with real treatment effects to obtain the optimal window. Our analysis concentrated on time windows that were precise multiples of seven, capturing and addressing the weekly patterns present in the data.

The average variance reduction results, presented as a function of the lookback window, are detailed below.

We selected a fixed 42-day lookback window for our analysis due to its balanced performance across all metrics and experiments. This timeframe presented a good trade-off between minimized variance while managing the computational costs of our metric pipeline. We also found that a 42-day window is good enough to capture average business dynamics, particularly encompassing at least one credit card billing cycle, ensuring a comprehensive view of customer behavior and financial activity. 

However, the actual impact of CUPED varies significantly across our diverse ecosystem. The chart below illustrates the distribution of variance reduction we observed in practice over hundreds of metrics in thousands of different experiment comparisons.

This distribution reveals a couple of key insights:

  • About 40% of comparisons had their variance reduced by more than 20%. In rare instances of specific populations and metrics, variance was reduced by almost 99%.
  • In about 12% of comparisons, there was no pre-experiment data available (NA). In the remaining cases, variance was reduced by less than 10%.

Ultimately, this shows that while finding a perfect, one-size-fits-all lookback window is challenging, our standardized 42-day period provides a meaningful reduction in variance for a vast majority of our metrics. It’s a challenge to find good, generic variance explainers.

Lesson 3: The shrinkage effect

The path to fully integrating CUPED wasn’t just a technical one. One of the primary reasons we initially hesitated to make it the default view in our experimentation platform was the shift in how the Average Treatment Effect (ATE) is interpreted. Although the new estimator is more precise, its point estimate also changes and it can significantly differ from the raw, unadjusted lift our teams were accustomed to.
This discrepancy is further complicated by our use of relative lift as the primary result in the platform, calculated as the ATE divided by the sample mean of the control group.

Explaining a metric that combines an adjusted numerator with an unadjusted denominator adds another layer of complexity. This required a significant investment in training and office-hours with our users and stakeholders to ensure that they understood not just that the results were more accurate, but why. This effort was crucial to build trust and prevent misinterpretation of the adjusted outcomes.

The following scatter plot compares the relative lift estimated by our standard analysis (“Regular Lift”) against the relative lift estimated using CUPED (“CUPED Lift”) across thousands of experiments and over hundreds of binary and continuous metrics.

The chart reveals three key insights. First, the dense cluster of points is symmetric around the y=x line. This empirical observation aligns with the theory that the CUPED-adjusted lift remains an unbiased estimator.

Second, light purple stars represent experiments where the standard analysis failed to find a statistically significant result, but the CUPED-adjusted analysis succeeded. Although statistical significance is an arbitrary definition (and we apply some conservative corrections like Bonferroni in our multiple-metric dashboards), these points might represent “hidden wins” and “missed warnings”, uncovered due  to CUPED’s ability to increase statistical power. Third, a closer look at these points also reveals a subtle but important “shrinkage effect”: the CUPED lift for these blue stars is often closer to zero than the estimate from the regular analysis. This shrinkage is not a weakness but a crucial feature that helps mitigate Type M (magnitude) errors. By attaining a higher power for experiments originally dimensioned for the naive estimator, CUPED shrinks the point estimate towards the true treatment effect in statistically significant comparisons. . These shrinked estimates are illustrated as dark purple triangles in the chart.

Conclusion

Nubank’s implementation of CUPED on its Experimentation Platform yielded 3 significant insights. First, it’s crucial to be aware of unequal variances and sample sizes between control and treatment groups, as using a pooled estimator for theta (θpooled) can decrease precision, especially when the treatment changes the serial covariance of the metric. The company switched to a more robust weighted average estimator (θ*) to address this, which improved the accuracy of their analysis.

Second, a standard 42-day lookback window was chosen as a practical trade-off. While the optimal pre-experiment data period varies with different metrics and experiment scenarios, this fixed window was found to effectively reduce variance for the majority of metrics while being computationally efficient.

Lastly, we observed a shrinkage effect where CUPED-adjusted lifts for statistically significant results are often closer to zero than the regular analysis. This is a valuable feature that helps mitigate Type M errors by correcting for overestimated effects and provides a more precise and realistic estimate.

Building on the success of CUPED, we are actively exploring new avenues for variance reduction. This includes investigating the incorporation of additional covariates and developing robust methodologies to platformize advanced variance reduction techniques at scale across all our experimentation efforts. Our ongoing work in this area reinforces Nubank’s commitment to leveraging sophisticated experimentation methods, ultimately supporting our mission to continuously make better product decisions that benefit our customers.

Bonus lesson

Implementing CUPED for Ratio Metrics

Given a ratio metric R = Y / Z, Var(R) can be estimated using the Delta Method theorem. This statistical approach is particularly useful for approximating the variance of functions of normal random variables. As detailed in Sections 2.2 and 3.2 of the paper by Nie et al. 2020 paper and in the formula below:

where n is the sample size. The most interesting challenge of this implementation lies in accurately determining the covariance of a ratio metric for calculating θ* . 

Consider two ratio metrics: Y/Z as the post-experiment metric and X/W as the pre-experiment metric. A first-order approximation for the covariance of two ratio metrics can be derived after linearization via Taylor expansion:

With the covariance between pre- and post-experiment ratio metrics calculated, the ATE and its variance can be estimated using the exact same procedure described in the introduction. 

The post 3 Lessons from implementing Controlled-Experiment Using Pre-Experiment Data (CUPED) at Nubank appeared first on Building Nubank.

Permalink

The programmers who live in Flatland

In the book Flatland: A Romance of Many Dimensions, a two-dimensional world called “Flatland” is inhabited by polygonal creatures like triangles, squares, and circles. The protaganist, a square, is visited by a sphere from the third dimension. He struggles to comprehend the existence of another dimension even as the sphere demonstrates impossible things. It’s a great book that has stuck with me since I first read it almost 30 years ago.

I’ve realized that “Flatland” is a perfect metaphor for the state of mind of a large number of programmers. Consider this: in 2001 Paul Graham, one of the most influential voices in tech, wrote the essay Beating the Averages. He argues forcefully about Lisp being fundamentally more powerful than other languages and credits Lisp as the key reason why his startup Viaweb outlasted their competitors. He identifies macros as the particularly distinguishing capability of Lisp. He writes:

A big chunk of our code was doing things that are very hard to do in other languages. The resulting software did things our competitors’ software couldn’t do. Maybe there was some kind of connection. I encourage you to follow that thread.

I did follow that thread, and that essay is a key reason why Clojure has been my primary programming language for the past 15 years. What Paul Graham described about the power of macros was absolutely true. Yet evidently very few shared my curiosity and Lisp/Clojure are used by a tiny percentage of programmers worldwide. How can this be?

Many point to “ecosystems” as the barrier, an argument that’s valid for Common Lisp but not for Clojure, which interops easily with one of the largest ecosystems in existence. So many misperceptions dominate, especially the reflexive reaction that the parentheses are “weird”. Most importantly, you almost never see these perceived costs weighed against Clojure’s huge benefits. Macros are the focus of this post, but Clojure’s approach to state and identity is also transformative. The scale of the advantages of Clojure dwarfs the scale of adoption.

In that essay Paul Graham introduced the “blub paradox” as an explanation for this disconnect. It’s a great metaphor I’ve referenced many times over the years. This post is my take on explaining this disconnect from another angle that complements the blub paradox.

I recognize there’s a fifty year history of Lisp programmers trying to communicate its power with limited success. So I don’t think I’m going to change any minds. Yet I feel compelled to write this since the metaphor of “Flatland” just fits too well.

Dimensions of programming

Programming revolves around abstractions, high-level ways of thinking about code far removed from the base primitives of bits, machine instructions, and memory hierarchies. But not all abstractions are created equal. Most abstractions programmers use are automations, a package that bundles a set of operations behind a name. A function is the canonical example: it takes inputs and produces an output. You don’t need to know how the function works and can think just in terms of the function’s spec and performance characteristics.

Then there are the rare abstractions which extend the algebra of programming itself: the basic concepts available and the kinds of relationships that can exist between them. These are the abstractions that create new dimensions.

Lisp/Clojure macros derive from the uniformity of the language to enable composing the language back on itself. Logic can be run at compile-time no differently than at runtime using all the same functions and techniques. The syntax tree of the language can be manipulated and transformed at will, enabling control over the semantics of code itself. The ability to manipulate compile-time so effortlessly is a new dimension of programming. This new dimension enables you to write fundamentally better code that you’ll never be able to achieve in a lower dimension.

If you’re already a Lisp programmer, you already understand the power of macros and how to avoid the potential pitfalls. My description is boring because you’ve done it a thousand times. But if you’re not a Lisp programmer, what I described probably sounds crazy and ill-advised!

In Flatland, the square cannot comprehend the third dimension because he can only think in 2D. Likewise, you cannot comprehend a new programming dimension because you don’t know how to think in that dimension. You do not have the representational machinery to understand what the new dimension is even offering. A programmer in 2D may conclude the 3D concept is objectively wrong. This isn’t because they’ve understood it, but because they’re trying to flatten it into their existing coordinate system.

Learning new dimensions

You can’t persuade someone in 2D with 3D arguments. This is exactly like how in Flatland the sphere is unable to get the square to comprehend what “up” and “down” mean.

However, this is where the metaphor breaks down. Though your brain will never be able to comprehend 4D space, your brain can adapt to new dimensions of programming. People who adopt Lisp/Clojure typically describe the experience similarly. First it’s uncomfortable, then there’s a series of moments of clarity, and then there’s a feeling they can never go back.

All it takes is curiosity and an understanding that the greatest programming ideas sometimes can’t be appreciated at first. That latter point is the key insight that gets lost in the conversation. We all have that cognitive bias. Recognizing that bias is enough to break it, and it’s one of the best ways to grow as a programmer. Macros are not the only great programming idea with this dimension-shifting quality.

Conclusion

In the end, living in Flatland is a choice. The choice appears the moment you notice that instinctive recoil from an unfamiliar idea, when you feel that tension between “this doesn’t make sense” and “maybe I don’t yet have the concepts to make sense of it.” What you do in that moment defines whether you stay in Flatland or step out of it.

Permalink

Aimless

I’ve been doing Clojure long enough that I remember when Clojure was just a JAR provided in a ZIP file. As I much as I benefit from all the work around modern Clojure tooling, it was less ceremonious in the old days to play around without big plans in mind.

During the 2025 Conj Clojure Tooling Working Group meetup (coordinated by the awesome Christoph Neumann), Rich Hickey said that Clojure should be useful without needing to create a formal project. That struck a chord and reminded me of the simpler times.

It also got me thinking how even experienced Clojurists still find ClojureScript difficult or unnatural to setup. That’s really unfortunate as António Monteiro, Mike Fikes, myself and others put in an incredible amount of work over the years to make using ClojureScript more like using Clojure. Let’s see what I mean. Edit your ~/.clojure/deps.edn to include the following:

{:aliases
 {:#cljs {:extra-deps
          {org.clojure./clojurescript {:mvn/version "1.12.116"}}
          :main-opts ["-m" "cljs.main" "-r"]}}}

That’s really all you need. No extra compiler flags, no build configuration EDN or files. Pick whatever global alias name makes sense for you. The idea is to not surprise yourself later. Go ahead and launch a ClojureScript REPL, it doesn’t matter where. Don’t worry, since we didn’t specify an output directory, ClojureScript will write to a tmp directory.

clj -M:#cljs

A browser window will open. Type a few things at the REPL. Then make a file called whatever you want but ending in .cljs. user.cljs works if you’re not feeling inspired. Again, we’re just aimlessly exploring - how does the new proxy functionality in ClojureScript work? Add the following to your file:

(require '[cljs.proxy :refer [builder]])

(def proxy (builder))
(def proxied-map (proxy {:first "Foo" :last "Bar"}))

Note we didn’t define a namespace. We don’t need to! Let’s load the file in the REPL with (load-file "user.cljs") or whatever name you came up with. Right-click on the browser REPL page to open the browser inspector (in Safari you’ll need to enable Developer tools). Navigate to the Console. Try typing the following into the JavaScript Console:

cljs.user.proxied_map["first"]
cljs.user.proxied_map["last"]
Object.keys(cljs.user.proxied_map)

That’s it! No project, no namespaces - just experiments and explorations. You never know what might happen. I actually wrote the code that became cljs.proxy without concrete goals in mind in exactly this way.

Permalink

Copyright © 2009, Planet Clojure. No rights reserved.
Planet Clojure is maintained by Baishamapayan Ghose.
Clojure and the Clojure logo are Copyright © 2008-2009, Rich Hickey.
Theme by Brajeshwar.