Writing your tests in EDN files

I've previously written about my latest approach to unit tests:

[Y]ou define only the input data for your function, and then the expected return value is generated by calling your function. The expected value is saved to an EDN file and checked into source, at which point you ensure the expected value is, in fact, what you expect. Then going forward, the unit test simply checks that what the function returns still matches what’s in the EDN file. If it’s supposed to change, you regenerate the EDN file and inspect it before committing.

I still like that general approach, however my previous implementation of it ended up being a little too heavy-weight/too reliant on inversion of control. The test runner code had all these things built into it for dealing with fixtures, providing a seeded database value, and other concerns. Writing new tests ended up requiring a little too much cognitive overhead, and I reverted back to manual testing (via a mix of the REPL and the browser).

I have now simplified the approach so that writing tests is basically the same as running code in the REPL, and there's barely anything baked into the test runner itself that you have to remember. I put all my tests in EDN files like this (named with the pattern my_namespace_test.edn):

{:require
 [[com.yakread.model.recommend :refer :all]
  [com.yakread.lib.test :as t]
  [clojure.data.generators :as gen]],
 :tests
 [{:eval (weight 0), :result 1.0}
  _
  {:eval (weight 1), :result 0.9355069850316178}
  _
  {:eval (weight 5), :result 0.7165313105737893}
  ...]}

(weight is a simple function for the forgetting curve, which I'm using in Yakread's recommendation algorithm.)

I only write the :eval part of each test case. The test runner evaluates that code, adds in the :result part, and pprints it all back to the test file. Right now there isn't a concept of "passing" or "failing" tests. Instead, when the tests are right, you check them into git; if any test results change, you'll see it in the diff. Then you can decide whether to commit the new results (if the change is expected) or go fix the bug (if it wasn't). If I had CI tests for my personal projects, I'd probably add a flag to have the test runner report any test cases with changed results as failed.

In my lib.test namespace I've added a couple helper functions, such as a t/with-db function that populates an in-memory XTDB database value:

{:require
 [[com.yakread.work.digest :refer :all]
  [com.yakread.lib.test :as t]
  [clojure.data.generators :as gen]],
 :tests
 [{:eval
   (t/with-db
    [db
     [{:xt/id "user1", :user/email "user1@example.com"}
      {:xt/id "user2",
       :user/email "user2@example.com",
       :user/digest-last-sent #time/instant "2000-01-01T00:00:00Z"}]]
    (queue-send-digest
     {:biff/db db,
      :biff/now #time/instant "2000-01-01T16:00:01Z",
      :biff/queues
      {:work.digest/send-digest
       (java.util.concurrent.PriorityBlockingQueue. 11 (fn [a b]))}}
     :start)),
   :result
   {:biff.pipe/next
    ({:biff.pipe/current :biff.pipe/queue,
      :biff.pipe.queue/id :work.digest/send-digest,
      :biff.pipe.queue/job
      {:user/email "user1@example.com", :xt/id "user1"}})}}
  ...]}

(queue-send-digest returns a list of users who need to be sent an email digest of their RSS subscriptions and other content.)

I like this approach a lot more than the old one: you just write regular code, with test helper functions for seeded databases or whatever if you need them. It's been pretty convenient to write my "REPL" code in these _test.edn files and then have the results auto-update as I develop the function under test.

There are a couple other doodads: if the code in :eval throws an exception, the test runner writes the exception as data into the test case, albeit under an :ex key instead of under :results:

{:require
 [[com.yakread.model.recommend :refer :all]
  [com.yakread.lib.test :as t]
  [clojure.data.generators :as gen]],
 :tests
 [{:eval (weight 0)
   :ex
   {:cause "oh no",
    :data {:it's "sluggo"},
    :via
    [{:type clojure.lang.ExceptionInfo,
      :message "oh no",
      :data {:it's "sluggo"},
      :at
      [com.yakread.model.recommend$eval75461$weight__75462
       invoke
       "recommend.clj"
       60]}],
    :trace
    [[com.yakread.model.recommend$eval75461$weight__75462
      invoke
      "recommend.clj"
      60]
     [tmp418706$eval83727 invokeStatic "NO_SOURCE_FILE" 0]
     [tmp418706$eval83727 invoke "NO_SOURCE_FILE" -1]
     [clojure.lang.Compiler eval "Compiler.java" 7700]
     [clojure.lang.Compiler eval "Compiler.java" 7655]
     [clojure.core$eval invokeStatic "core.clj" 3232]
     [clojure.core$eval invoke "core.clj" 3228]]}}
  ...]}

The stack trace gets truncated so it only contains frames from your :eval code (mostly—I could truncate it a little more).

I also capture any tap>'d values and insert those into the test case, whether or not there was an exception. It's handy for inspecting intermediate values:

:tests
[{:eval (weight 1),
  :result 0.9355069850316178,
  :tapped ["hello there" "exponent: -1/15"]}
  ...

And that's it. If you want to try this out, you can copy run-examples! (the test runner function) into your own project. It searches your classpath for any files ending in _test.edn and runs the tests therein. I call it from a file watcher (Biff's on-save function) so your test results get updated whenever you save any file in the project.

Permalink

My programming environment journey

No one actually cares about my programming environment journey, but I’ve often been asked to share it, perhaps for the sake of social media algorithms. I post it here, so later, I can copy and paste this conveniently.

My first computer, in the sense that I, not someone else, made the decision to buy it, ran Debian in 2002. It was a used Compaq desktop with a Pentium II processor, which I bought from Zeer Rangsit, a used computer market that may be the most famous in Thailand these days. When I got it home, I installed Debian right away. Before I bought my computer, I had used MBasic, mainly MS-DOS, Windows 3.1 (though rarely), and Solaris (remotely). For experimentation, I used Xenix, AIX, and one on DEC PDP-11 that I forgot.

Since I started with MBasic, that was my first programming environment. I learned Logo at a summer camp, so that became my second. Later, my father bought me a copy of Turbo Basic, and at school, I switched to Turbo Pascal.

After moving to GNU/Linux, I used more editors instead IDEs. From 1995 to 2010, my editors were pico, nvi, vim, TextMate, and Emacs paired with GCC (mostly C, not C++), PHP, Perl, Ruby, Python, JavaScript, and SQL. I also used VisualAge to learn Java in the 90s. I tried Haskell, OCaml, Objective C, Lua, Julia, and Scala too, but it was strictly for learning only.

After 2010, I used IntelliJ IDEA and Eclipse for Java and Kotlin. For Rust (instead of C), I used Emacs and Visual Studio Code. I explored Racket for learning purposes, then later started coding seriously in Clojure and Common Lisp. I tried using Vim 9.x and Neovim too, they were great, but not quite my cup of tea.

In 2025, a few days ago, I learned Smalltalk with Pharo to deepen my understanding of OOP and exploratory programming.

Permalink

When You Get to Be Smart Writing a Macro

Day-to-day programming isn’t always exciting. Most of the code we write is pretty straightforward: open a file, apply a function, commit a transaction, send JSON. Finding a problem that can be solved not the hard way, but smart way, is quite rare. I’m really happy I found this one.

I’ve been using hashp for debugging for a long time. Think of it as a better println. Instead of writing

(println "x" x)

you write

#p x

It returns the original value, is shorter to write, and doesn’t add an extra level of parentheses. All good. It even prints original form, so you know which value came from where.

Under the hood, it’s basically:

(defn hashp [form]
  `(let [res# ~form]
     (println '~form res#)
     res#))

Nothing mind-blowing. It behaves like a macro but is substituted through a reader tag, so defn instead of defmacro.

Okay. Now for the fun stuff. What happens if I add it to a thread-first macro? Nothing good:

user=> (-> 1 inc inc #p (* 10) inc inc)
Syntax error macroexpanding clojure.core/let at (REPL:1:1).
(inc (inc 1)) - failed: vector? at: [:bindings] spec: :clojure.core.specs.alpha/bindings

Makes sense. Reader tags are expanded first, so it replaced inc with (let [...] ...) and then tried to do threading. Wouldn’t fly.

We can invent a macro that would work, though:

(defn p->-impl [first-arg form fn & args]
  (let [res (apply fn first-arg args)]
    (println "#p->" form "=>" res)
    res))

(defn p-> [form]
  (list* 'p->-impl (list 'quote form) form))

(set! *data-readers* (assoc *data-readers* 'p-> #'p->))

Then it will expand to

user=> '(-> 1 inc inc #p-> (* 10) inc inc)

(-> 1
  inc
  inc
  (p->-impl '(* 10) * 10)
  inc
  inc)

and, ultimately, work:

user=> (-> 1 inc inc #p-> (* 10) inc inc)
#p-> (* 10) => 30
32

Problem? It’s a different macro. We’ll need another one for ->>, too, so three in total. Can we make just one instead?

Turns out you can!

Trick is to use a probe. We produce an anonymous function with two arguments. Then we call it in place with one argument (::undef) and see where other argument goes.

Inside, we check where ::undef lands: first position means we’re inside ->>, otherwise, ->:

((fn [x y]
   (cond
     (= ::undef x) <thread-last>
     (= ::undef y) <thread-first>))
 ::undef)

Let’s see how it behaves:

(macroexpand-1
  '(-> "input"
     ((fn [x y]
        (cond
          (= ::undef x) <thread-last>
          (= ::undef y) <thread-first>))
      ::undef)))

((fn [x y]
   (cond
     (= ::undef x) <thread-last>
     (= ::undef y) <thread-first>))
   "input" ::undef)

(macroexpand-1
  '(->> "input"
     ((fn [x y]
        (cond
          (= ::undef x) <thread-last>
          (= ::undef y) <thread-first>))
      ::undef)))

((fn [x y]
   (cond
     (= ::undef x) <thread-last>
     (= ::undef y) <thread-first>))
   ::undef "input")

If we’re not inside any thread first/last macro, then no substitution will happen and our function will just be called with a single ::undef argument. We handle this by providing an additional arity:

((fn
   ([_]
    <normal>)
   ([x y]
    (cond
      (= ::undef x) <thread-last>
      (= ::undef y) <thread-first>)))
   ::undef)

And boom:

user=> #p (- 10)
#p (- 10)
-10

user=> (-> 1 inc inc #p (- 10) inc inc)
#p (- 10)
-7

user=> (->> 1 inc inc #p (- 10) inc inc)
#p (- 10)
7

#p was already very good. Now it’s unstoppable.

You can get it as part of Clojure+.

Permalink

Remote Clojure(Script) Engineer at Lifecheq

Remote Clojure(Script) Engineer at Lifecheq

eur45000 - eur55000

What are we all about?

Lifecheq is a modern fintech company transforming personal finance through technology. Headquartered in South Africa and operating remotely, we’re building a category-defining advice platform used by individuals, financial advisers, and large enterprises.

We’re a Clojure-centric engineering team of around 25 people, working closely with product managers and designers in cross-functional squads. Most of us are full stack, and we care deeply about writing thoughtful, composable systems. Our tech stack is built around Clojure, ClojureScript, event sourcing, GraphQL, and functional programming principles.

Lifecheq is backed by institutional investors including Naspers Foundry, African Rainbow Capital, and Futuregrowth. We’re solving meaningful problems with real-world impact — helping people make better financial decisions through powerful, user-friendly tools.

How you’ll grow in this role

  • You will work in a cross functional squad, along with non-Clojure engineers, financial experts, designers and product people. Each squad with a clear focus on a set of customer related goals and outcomes, and autonomy in how to best realise that. With great autonomy comes great responsibility.

  • Personal financial advice is one of the toughest business domains to crack: it will flex your design and coding skills, it will challenge how you test, integrate and deploy.

  • As we continue growing, contribute to how we architect, deploy and monitor our systems.

  • Learn with our 21 people strong Clojure team, that has an average of 14 years of experience;

  • We’re scaling our operations significantly: grow with the company, and choose your technical or management path.

What It’s Like to Work Here

  • You'll be part of a focused, collaborative squad with strong ownership and autonomy

  • Expect a friendly, low-ego environment where good engineering practices matter

  • You’ll get a dedicated engineering manager, and an onboarding buddy to support you through your first few months

  • We favour clear communication, async workflows, and time for deep work

  • Core hours are aligned to UTC+2, but we're remote-friendly and flexible

What skills you’ll bring

  • Minimum 3 years of professional software development experience, fluency in core Clojure/ClojureScript;

  • Flexibility across frontend/backend is a big plus, but specialisation in either is also welcome;

  • Energy, joy, curiosity.

Tech Stack

  • Languages: Clojure, ClojureScript, TypeScript, Python

  • Infra: AWS, PostgreSQL, Spark, S3, Delta Lake

  • Frameworks: Lacinia (GraphQL), re-frame, sci

  • Practices: Event sourcing, immutable data, functional design

Compensation & Location

Remote, with a preference for time zones overlapping UTC+2. We hire internationally on a contract basis.

Permalink

Mid-Level Engineer transitioning to Clojure (Remote) at Lifecheq

Mid-Level Engineer transitioning to Clojure (Remote) at Lifecheq


Have you watched all of Rich Hickey’s talks more than once – and actually started to get it? If you’re learning Clojure and feel that something has clicked, we want to hear from you.

This is a rare opportunity to receive full salary from day one while being mentored by developers with over 15 years of Clojure experience. Over three months, you’ll get focused, practical training in the language and its core libraries: core.async, re-frame, HoneySQL, and more.

This is about more than syntax or tools. You’ll learn to think the Clojure way: how to design systems with simplicity, leverage data over objects, and use the REPL to shape your programs interactively.

We don’t overload you with theory: within 2-3 weeks, you’ll be shipping real Clojure code for carefully scoped tasks to match what you’ve just learned. This is a chance to gain hands-on experience, sharpen your functional mindset, and level up fast with guidance from people who’ve walked the path.

Your first three months will focus on onboarding and growth, setting you up for long-term success at Lifecheq. After this period, you’ll join one of our squads as a full-time mid-level Clojure developer, contributing to our mission to improve the lives of people in developing countries by giving them access to high-quality financial advice.

Who we are

Lifecheq is a personal finance fintech business based in South Africa, operating across the region. Our advice platform touches consumers, financial advisers, and large enterprise clients. We're backed by reputable institutional investors like Naspers, Futuregrowth, and African Rainbow Capital — all aligned with our mission to help people make better financial decisions.

What to expect

  • Full salary from day one
  • 3 months dedicated to training
  • You’ll have a dedicated mentor: a senior developer with 22 years of experience, 15 of them in Clojure, focused on supporting your growth.
  • Alternating periods of focused learning and real production work, matched to your level.
  • After training, join a developer squad and keep growing by solving real problems alongside a highly experienced Clojure team.

Must have

  • 5 years professional experience as a software developer
  • Clojure/ClojureScript (or other functional language) at the hobbyist level
  • Comfort with REST APIs and relational databases
  • Familiarity with Git, unit testing, and clean code practices
  • Fluency in English
  • Live anywhere from EMEA up to India

Permalink

Inside the role: What does a Chief of Staff do at Nubank?

There’s a kind of leadership that rarely steps into the spotlight, yet its presence is essential for everything to run smoothly and strategically. It’s quiet, thoughtful, deeply connected to people, the business, and the decisions that move an organization forward.

At Nubank, and in many other companies, this role goes by a name that’s gaining more relevance every day: Chief of Staff.

While often associated with governance and meeting management, the Chief of Staff role is much more than that. At its best, it is inherently strategic — bridging vision and execution, anticipating risks, clarifying priorities, and ensuring leaders stay focused on what truly matters. It’s the hidden engine that helps organizations move forward with clarity and cohesion.

We spoke with Cristina Otto and Sheila Oliveira, who play this role in different contexts at Nu, but share a core trait: their impact doesn’t come from the spotlight, but from orchestrating others to shine. They turn ideas into action, create space for others to lead, and help strategies unfold without losing their essence.

“The Chief of Staff is the invisible force that turns strategy into execution.”

Bridge and radar

Cristina compares her role to a bridge. She connects leaders to teams, areas to priorities, and decisions to outcomes. She also acts as a filter, managing operational tasks and bringing to the executive’s attention only what truly matters.

Sheila sees the role simultaneously as an emotional and strategic radar. “We manage through influence,” she says. In a highly complex environment, where no one reports directly to you, trust opens doors. Empathetic listening, understanding nuances, knowing when to intervene, and, above all, respecting others’ space while carving out your own are key.

Day-to-day at Nu: Context, change, and connection

If there’s a routine, it’s one of strategic improvisation. Each week might start with one plan and end with another—but always guided by purpose. A Chief of Staff doesn’t merely react to changes; they skillfully capture business nuances, anticipate risks, and recalibrate strategies proactively.

“Sometimes we arrive with our own backlog and leave meetings with an entirely different list,” Sheila says. This isn’t just rearranging schedules—it’s strategically realigning based on leaders’ insights and market dynamics. It’s about pinpointing precisely which moves matter most for the company’s strategic goals.

Cristina echoes a similar approach: “Every Monday, I review my executive’s calendar and ask: is there anything I can facilitate? This single conversation can shift our entire week’s focus.” More than calendar management, it ensures every action aligns with the company’s vision and responds to emerging challenges.

Navigating various topics, prioritizing effectively, interpreting business demands, and swiftly shifting context—these are essential skills of Chiefs of Staff. Comfort with uncertainty and sharp strategic perception are critical: it’s not enough to manage change; you must clearly understand what each change means for long-term success.

Influence without authority

Chiefs of Staff are responsible for facilitating decisions, anticipating risks and ensuring alignment between areas. This requires influence built on trust and consistency of delivery.

“We’re accountable for many things without direct reports. Influence is everything. People pay attention if you genuinely add value,” says Cristina.

Sheila stresses the importance of adapting communication according to the audience and context. “For teams, more is more. For executives, less is more,” she says. This model is fundamental because each level of the organization needs different levels of context to make effective decisions.

Executives generally have limited time and need quick strategic clarity. Too much information at this level can create noise and loss of attention. Operational teams, on the other hand, need a deeper understanding of the reasons behind decisions in order to execute them effectively and purposefully.

The value of a Chief of Staff lies precisely in adjusting the message precisely, understanding the specific needs of each group. Sheila explains: “You need to know exactly what amount and type of information to deliver to ensure that everyone is aligned, engaged and clear enough to act.”

Measuring behind-the-scenes impact

Perhaps the role’s biggest challenge is creating significant impact without necessarily getting direct credit. It’s a role that shines precisely by not standing out.

Cristina measures success by creating an environment where better decisions are made and problems are anticipated before they arise. “Success means nothing breaking. It means the executive has sufficient context to lead clearly, priorities are well-defined, and teams can focus on execution,” she explains. This impact translates directly into the company’s operational pace, preventing bottlenecks that could jeopardize strategic results.

Sheila highlights how this impact extends beyond the immediate executive leadership to teams’ performance and daily operations. “My role ensures teams translate strategic intentions into practical actions. Removing internal barriers, accelerating decisions, and aligning priorities directly result in agile execution and tangible business outcomes,” she adds.

The impact is more than facilitating leaders’ daily routines; it fosters an environment where everyone clearly understands their role within the broader strategy, enabling the business to operate with greater efficiency, agility, and results in both short and long-term.

Career without a manual

There’s no single path to becoming a Chief of Staff. Cristina transitioned from engineering and product management. Sheila spent two decades as an executive assistant to CEOs. Within and beyond Nubank, Chiefs of Staff commonly come from Product, Finance, Communications, Consulting, and Operations—proving diverse backgrounds are more of an advantage than a barrier.

What truly matters are the skills cultivated throughout one’s career and required daily by the role: stakeholder management, organization and clarity, proactive risk detection (“smoke detection”), strong analytical and problem-solving abilities, sharp business sense, and the capability to succinctly communicate complex information (executive summary).

These skills merge in a role Sheila defines as a leadership accelerator. “It’s a position that prepares you for any other,” she summarizes, “but it can also be a rewarding destination for those who thrive on this type of challenge.”

For those interested in this path

Their final advice is an invitation: don’t wait to feel ready before trying. “Speak with people already in this role. Understand different styles. Find the one that suits you,” recommends Cristina.

“Don’t think you need to start at the top. Start from where you are. I’ve also been in that place of feeling I wasn’t enough,” says Sheila, reminding us that doubt often accompanies those who have the most to offer.

Ultimately, perhaps the greatest secret of the Chief of Staff is this: being the connector. Linking vision and execution, strategy and care, clarity and trust.

It’s a quiet yet decisive role. Sometimes invisible, always indispensable. And when done well, it transforms everything around it.

The post Inside the role: What does a Chief of Staff do at Nubank? appeared first on Building Nubank.

Permalink

Clojure as a First Language

This might come as somewhat of a shock to my regular audience, but I don't only write Java. The reason I focus so much on specifically Java education is because it is often the first language people are taught. This matters for a lot of reasons, not all of which I have the page space to get in to, but crucially being taught first has a direct impact on a language's popularity. I'm writing this because I think Clojure now has a real shot at becoming a first language. ## Why Of all the less-than-massively-popular-languages out there, Clojure is probably hurt the least by not having widespread adoption. This is in large part because it gets to piggyback on existing libraries and ecosystems. Clojure is a hosted language. Clojure on the JVM can use any Java library, Clojure in the browser can use any JavaScript library, and so on. It is always good when Clojure-native libraries get written, but they've never been an absolute necessity. So why pursue popularity? ### 1. Paper Clipping. A lawyer has a duty to be a zealous advocate for their client. There's a non-trivial tribal monkey aspect to wanting __your__ programming language to be popular and, at a certain point, pursuing that end [has no fundamental justification](https://en.wikipedia.org/wiki/Is%E2%80%93ought_problem). But we make our own meaning in life so screw it. ### 2. It's Better. The value proposition of Clojure is different to most other languages. I'm not going to s*** Paul Graham's d*** or wax poetic about macros, but it's hard to deny that Clojure codebases tend to be quite different to those written in Python, JavaScript, Java, R, etc. There are reasons to think that Clojure could be a better fit for producing certain genres of software. Popularity would therefore lead to "better" software being produced, which is reasonable to want. I'm being vague on purpose here. The difference between closed and open aggregates alone could be its own essay. ### 3. Money. The more people who use Clojure the more Clojure jobs there are. The more Clojure jobs there are the more secure Clojure experts can be, etc. TypedClojure is a one-man-show. TypeScript is funded by Microsoft paychecks. Popularity opens the door to all sorts of support. ## How So to become popular you need to be someone's first language. To be someone's first language you need to be what they learned in school. This means convincing teachers and curriculum makers. For CS education I think this is a lost cause. There are very well-made CS 101 courses that use lisps and [face cosmically stupid pushback for doing so](https://felleisen.org/matthias/Thoughts/py.html). It is hard to imagine winning that fight. But people going for Computer Science degrees aren't the only people who program. When my brother got his Masters in Marine Biology he was taught R. Analyzing data, making charts, etc. [is needed for a wide variety of fields](https://waterwelljournal.com/a-historical-perspective-of-well-installation/). I think Clojure could steal significant market share here. [Noj](https://scicloj.github.io/noj/) is a collection of a bunch of data science libraries for Clojure. With what is in there you can: * Load data into data frames * Make visualizations * Train ML models (including deep learning) * Do anything Python can do ([by directly calling Python libraries](https://scicloj.github.io/noj/noj_book.underlying_libraries.html#:~:text=Python%20bindings%20and%20interoperability)) * Do anything R can do ([by directly calling R libraries](https://scicloj.github.io/noj/noj_book.underlying_libraries.html#:~:text=R%20language%20interoperability%20and%20bindings)) * Do anything Wolfram can do ([by directly calling Worlfram](https://scicloj.github.io/wolframite/)) * ... and more This has the makings of an extremely compelling pitch. The biggest missing piece, as I see it, is resources tailored to people learning Clojure as a first language. Historically the vast majority of Clojure programmers have been transplants from other languages. We have books like [Clojure for the Brave and True](https://www.braveclojure.com/) that serve this crowd, but few-to-none for people who are starting truly from scratch. This has also affected the way people talk about Clojure. Keep in mind that you don't need to convince people of functional programming, homoiconicity, or anything else when it's the first thing they learn. The pitches you'd use to pull in a Ruby programmer should be kept in their lane. So that's the gap. The data science people are presumably going to keep chugging away at the things they do. If you are interested in making Clojure a "First Language" give a shot at making something that fills that gap.

Permalink

Simulating 1-D Convection in Clojure — From Equations to Arrays

Earlier this year I gave a talk at the first online Scinoj Light Conference, sharing a ongoing project to port Computational Fluid Dynamics(CFD) learning materials from Python to Clojure.

In this post, I’ll demonstrate a simple one-dimensional linear convection simulation implemented in Clojure using Java primitive arrays. This example shows a glimpse into the porting effort, with a focus on expressing numerical simulations using only built-in Clojure functions.

The Equation

(This section won’t take up too much of your time…)

We’re going to simulate the 1-D linear convection equation:

This explains how the flow velocity u changes over time t and position x, with c which is the wave speed.

Instead of focusing on the physical interpretation(since I am no expert), the main focus on this post will be its implementation side - expressing it numerically and coding it in Clojure!

Using a finite-difference scheme, the discretized form becomes:

Then, solving for u_i^{n+1} gives:

Initial Setup

We begin by defining initial simulation parameters to start:

  • nx: number of sliced steps for spatial point x from x-start and x-end
  • nt: number of time steps we want to propagate
  • dx: each sliced x calculated from dx = (x-end - x-start) / (nx - 1)
  • dt: sliced each time step
  • c: speed of wave
(def init-params
  {:x-start 0
   :x-end   2
   :nx      41
   :nt      25
   :dx      (/ (- 2 0) (dec 41))
   :dt      0.025
   :c       1})

Creating the x Grid

With the given init-params we defined earlier, we create a float-array of spatial points x:

(def array-x (let [{:keys [nx x-start x-end]} init-params
                   arr  (float-array nx)
                   step (/ (- x-end x-start) (dec nx))]
               (dotimes [i nx]
                 (aset arr i (float (* i step))))
               arr))
[0.0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6,
 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1.0, 1.05, 1.1, 1.15, 1.2,
 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8,
 1.85, 1.9, 1.95, 2.0]

Defining Initial Flow Velocity Condition

The initial flow velocity(when t = 0) is 2 when x ∈ [0.5, 1.0], and is 1 elsewhere:

(def init-cond-fn #(float (if (and (>= % 0.5) (<= % 1.0)) 2 1)))
(def array-u
  (let [nx (:nx init-params)
        u  (float-array nx)]
    (dotimes [i nx]
      (let [x-i (aget array-x i)
            u-i (init-cond-fn x-i)]
        (aset u i u-i)))
    u))
(def init-array-u (float-array array-u))
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0,
 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]

We can visualize this initial u:

Wait, Why dotimes?

Since we’re working with mutable Java arrays(float-array), dotimes is an efficient choice here. Because it gives direct, index-based iteration. And it pairs naturally with aget and aset for reading and writing array values.

Implementing and the Simulation

With the initial setup complete, we now apply the discretized convection equation at each time step.

Step Function

Given the previous time step’s array-u and the init-params, We compute and mutate the flow velocity in-place:

(defn mutate-linear-convection-u
  [array-u {:keys [nx c dx dt]}]
  (let [u_i (float-array array-u)]
    (dotimes [i (- nx 2)]
      (let [idx     (inc i)
            un-i    (aget u_i idx)
            un-i-1  (aget u_i i)
            new-u-i (float (- un-i (* c (/ dt dx) (- un-i un-i-1))))]
        (aset array-u idx new-u-i))))
  array-u)

Time Integration

We run the step function nt times to run our simulation over time.

(defn simulate!
  [array-u {:keys [nt] :as init-params}]
  (loop [n 0]
    (if (= n nt)
      array-u
      (do (mutate-linear-convection-u array-u init-params) (recur (inc n))))))

Finally, we visualize the resulting array-u:

(simulate! array-u init-params)
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0000007,
 1.0000097, 1.0000782, 1.0004553, 1.0020387, 1.0073167, 1.0216427,
 1.0538762, 1.1147615, 1.2121781, 1.3450189, 1.4999992, 1.6549712,
 1.7877436, 1.8847833, 1.9440854, 1.9710407, 1.9710407, 1.9440854,
 1.8847833, 1.7877436, 1.6549712, 1.4999992, 1.3450189, 1.2121781,
 1.1147615, 1.0538762, 1.0216427, 1.0073167, 1.0]

The plot shows how the flow velocity shifts from left to right over time, while also becoming smoother. Nice!

The Summary

This Simple example demonstrates a simulation process using low-level Java primitive arrays in Clojure.

Choosing this approach provided mutable, non-persistent data structures. While this deviates from idiomatic Clojure, it offers significant performance benefits for big-size numerical simulations. However, it comes with trade-offs; by opting for mutability, we give up the guarantees of immutability and structural sharing, making the code less safe and more error-prone.

As the porting project continues, we plan to evolve the design to better align with idiomatic Clojure principles. Stay tuned!

Permalink

Why choose Nu: A company that values your growth and impact

At Nu, we build careers with purpose. As we scale, we continue to invest in a culture that encourages autonomy, accelerates development, and drives meaningful change. One of the ways we’re doing this is through Purple Rockets, our new early-career development program exclusively for Nu Mexico.

Designed for emerging professionals, the program offers hands-on experience, mentorship, and the freedom to lead from day one. Here, your confidence grows, your ideas are valued, and your work makes a real difference.

If you’re passionate about technology, product design, analytics, or business strategy, you’ll find a career path at Nu where growth is a priority. And if you’re just getting started, Purple Rockets is your ideal entry point.

In this post, we share why Nu — and Purple Rockets — is the right place for those who seek purpose, autonomy, and impact.

Our mission 

Nu was born to combat complexity and empower people’s financial lives. This mission guides our decisions and is reflected in a culture that challenges the conventional and delivers the best products to our customers. 

When building our products, we always start with empathy — striving to deeply understand people’s needs so we can create solutions that truly make a difference. We innovate with courage, challenging the status quo and taking ownership, whether individually or as a team. We believe the best solutions are born from genuine collaboration and a constant drive to transform the world around us.

A culture that includes and fosters growth

Here, leadership does not depend on position, but on the ability to solve complex problems, generate impact and assume responsibility. Many of our most experienced professionals started out in entry-level positions and grew up facing real challenges.

We encourage autonomy with purpose. Growth happens in practice: by tackling problems that require critical thinking, data-driven decisions and scalable solutions.

These challenges can range from improving processes that impact millions of clients to developing products that democratize access to credit in Latin America.

Those who choose to take on these responsibilities find room to experiment, learn from the results and grow continuously, as professionals and as agents of change.

About Purple Rockets

If you’re at the beginning of your career and want to grow with purpose and speed, Purple Rockets is your launchpad at Nu Mexico. This is a structured talent acceleration program created for early-career professionals who have recently graduated and already gained some industry experience, and who are eager to rapidly advance their careers.

Why choose Purple Rockets? Because it’s more than a structured path — it’s a real opportunity to work on meaningful projects, alongside people who believe in your potential, in a company that gives you the freedom to lead.

You’ll gain hands-on experience in your specific area of focus, such as engineering, product management, product operations, or analytics. Participants receive mentorship from experienced professionals, face real-world challenges, and have the chance to make an impact from the start.

Purple Rockets is a springboard for those who want to lead, grow, and help shape the future of finance in Latin America.

Take the next step

Choosing where to start — or accelerate — your career is a big decision. At Nu, we don’t just offer jobs. We offer development, purpose, and the opportunity to be part of something meaningful.

With Purple Rockets, you’ll join a team that values bold ideas and gives you the autonomy to shape your own path.

Your future starts here. Apply now to Purple Rockets and take the lead in your career journey.

The post Why choose Nu: A company that values your growth and impact appeared first on Building Nubank.

Permalink

keep-indexed and map-indexed in Clojure

Code

;; keep_indexed.clj
;; https://clojuredocs.org/clojure.core/keep-indexed

(def some-vector  [1 17 5 7 6 8 2 12 11 8])

(def number 8)

;; %1 means index
;; %2 means value in the collection
(keep-indexed #(when (odd? %1) %2) [:a :b :c :d :e])

(map-indexed #(when (odd? %1) %2) [:a :b :c :d :e])

(keep-indexed #(if (pos? %2) %1) [-9 0 29 -7 45 3 -8])

(map-indexed #(if (pos? %2) %1) [-9 0 29 -7 45 3 -8])

;; What they mean by pred here?
(defn indices [pred coll]
  (keep-indexed #(when (pred %2) %1) coll))

(indices #(= % number) some-vector)

;; % means %1, that is the index
(map-indexed #(when (< % 2) (str % %2)) [:a :b :c])

(keep-indexed #(when (< % 2) (str % %2)) [:a :b :c])

Permalink

Poor man's bitemporal data system in SQLite and Clojure

On trying to mash up SQLite with ideas stolen from Accountants, Clojure, Datomic, XTDB, Rama, and Local-first-ers, to satisfy Henderson's Tenth Law. Viz., to make a sufficiently complicated data system containing an ad-hoc, informally-specified, bug-ridden, slow implementation of half of a bitemporal database. Because? Because laying about on a hammock, contemplating hopelessly complected objects like Current Databases isn't just for the Rich man.

Permalink

Clojure Deref (July 14, 2025)

Welcome to the Clojure Deref! This is a weekly link/news roundup for the Clojure ecosystem (feed: RSS).

The Clojure/conj 2025 Call for Presentations is open now until July 27! We are seeking proposals for both 30 minute sessions and 10 minute lightning talks.

Libraries and Tools

New releases and tools this week:

  • legba - Clojure library for building OpenAPI services

  • babashka 1.12.205 - Native, fast starting Clojure interpreter for scripting

  • cider 1.19.0 - The Clojure Interactive Development Environment that Rocks for Emacs

  • datomic-browser - web-based support/diagnostics UI for any production Datomic service

  • repl-mcp - Model Context Protocol Clojure support including REPL integration with development tools.

  • teensyp - GitHub - weavejester/teensyp

  • huff 0.2.21 - Juicy hiccup in pure Clojure

  • mcp-server - MCP Server library

  • ring-nexus-middleware - Ring FCIS (Functional Core Imperative Shell) support through nexus

  • clay 2-beta48 - A REPL-friendly Clojure tool for notebooks and datavis

  • qclojure 0.8.0 - A functional quantum computer programming library for Clojure with backend protocols, simulation backends and visualizations.

  • lazytest 1.8.0 - A standalone BDD test framework for Clojure

  • calva 2.0.521 - Clojure & ClojureScript Interactive Programming for VS Code

  • pretty 2.3.0 - Library for helping print things prettily, in Clojure - ANSI fonts, formatted exceptions

  • squint 0.8.150 - Light-weight ClojureScript dialect

  • cherry 0.4.29 - Experimental ClojureScript to ES6 module compiler

  • cli 0.8.66 - Turn Clojure functions into CLIs!

Permalink

Lindenmeyer systems playground

Not long ago I learned about Replicant, so as an excuse to play around with it I created a playground for Lindenmeyer systems based on my previous posts about L-systems. You can use the playground here.

While building it, I added support for pushing and popping states with the [ and ] operators, which turned out to be relatively straight-forward. The conversion of commands such as [:turn 90] into a set of points looks like this:

(defmulti update-state (fn [_ [cmd]] cmd))
(defmethod update-state nil [state _]
  state)
(defmethod update-state :forward [state [_ param]]
  (let [{[x y] :point :keys [heading points]} state
        radians (* heading Math/PI (/ 180))
        x (+ x (* param (Math/sin radians)))
        y (+ y (* param (Math/cos radians)))]
    (assoc state :point [x y] :points (conj points [x y]))))
(defmethod update-state :turn [state [_ param]]
  (assoc state :heading (- (state :heading) param)))
(defmethod update-state :push-state [state _]
  (update state :states conj {:point (state :point) :heading (state :heading)}))
(defmethod update-state :pop-state [state _]
  (let [{:keys [point heading]} (-> state :states peek)]
    (-> state
        (assoc :point point :heading heading)
        (update :points conj :move point)
        (update :states pop))))
(defn coords [cmds]
  (:points
    (reduce update-state
            {:point [0 0]
             :heading 180
             :points [[0 0]]
             :states []}
            cmds)))

Note that when popping the state the list of points gets the non-coordinate value :move, which is recognized by the drawing code and tells it not to draw a line segment from the previous point to the new point.

I also improved the generation of random L-systems. I've noted some symmetries in the rules of systems that produce pleasing outputs, but in the playground I created symmetries by transforming a randomly generated base rule, then selectively reversing it, inverting it, and concatenating with the original rule to create new, related rules. It will also occasionally insert [ and ] commands, though not always in a meaningful way (e.g., [+]). The current system seems to have the best success rate of any of my previous attempts, more frequently generating non-trivial and visually distinctive patterns.

If you have suggestions for further improvements, please let me know.

Permalink

Clojurists Together project - Scicloj - Building Bridges to New Clojure Users - July 2025 update

Clojurists Together announced a sponsorship for a project by Siyoung Byun – Scicloj - Building Bridges to New Clojure Users for Q2 2025. Here is a summary of the first update for the project - any comments would be greatly appreciated 🙏 Clojurists Together update - July 2025 - Siyoung Byun # Work Completed, In Progress and Further Plans # CFD Python into Clojure Project # I initiated the CFD Python into Clojure project, which translates computational fluid dynamics (CFD) learning steps from Python into Clojure.

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.