HN user

hlship

163 karma

Senior Staff Software Engineer at NuBank NA.

Doing all the Clojure.

Remember Apache Tapestry? That was me.

Posts2
Comments43
View on HN
Why Clojure? 1 year ago

I've been working in Clojure now for about 12 years. Maybe 12+ years of Java prior to that.

I've created some great apps, and great libraries (in both Clojure and Java).

I often describe Clojure as "the least worst programming language", which is an off-handed complement, but I think accurate. Things you don't like can generally be fixed (at least locally) using macros and libraries. The core is strong, a good basis for building things ... and as described all over this thread, stable.

As you master your tools, you gain a level of speed and precision in your work that I have not found elsewhere. The REPL-oriented workflow is a central value proposition in Clojure, and many features (and a few limitations) of the language exist to properly support it.

Working in Clojure feels like I'm working "with" my code, molding it like clay. My prior experiences in Java and Objective-C were so much slower, with long code-compile-test-debug cycles that are compressed down to instantaneous responses in the running REPL.

Why Clojure? 1 year ago

The cli/deps.edn tooling is different from Leiningen, and far, far less complicated IMO. I've written a good number of Leiningen plugins and it was always brutal to get anything to work properly, especially in combination with other plugins.

Leiningen attempts to be everything to everyone in terms of building, testing, and packaging Clojure code. It's Clojure's version of Maven.

cli/deps.edn effectively reduces things down to a) what should be on the classpath and b) what should get executed. Working inside an IDE? You just want it to download the dependencies and build a classpath. Running nREPL? Add that to the classpath, and set the starting namespace to start nREPL. Packaging an application? Run dependencies that do that work, based on the clojure.tools.build library.

I was there myself; I used Leiningen and didn't take the time to figure out deps.edn --- until I did, saw the light, and converted all of Walmart's projects to use deps.edn, which greatly sped up our build and improved our dev experience.

Why Clojure? 1 year ago

~ > time bb -e '(println "Hello World")' Hello World bb -e '(println "Hello World")' 0.01s user 0.03s system 18% cpu 0.212 total

bb is Babashka: a pre-compiled (using Graal), interpreted, scripting language version of Clojure.

In terms of realistic work:

~/workspaces/github/pedestal/tests > time clj -X:test {:in user/eval2321, :line 13, :thread "main", :dev-mode? false}

Running tests in #{"test"}

...

Testing io.pedestal.test-test

Ran 391 tests containing 1180 assertions. 0 failures, 0 errors. clj -X:test 30.15s user 2.04s system 203% cpu 15.838 total ~/workspaces/github/pedestal/tests > java -version openjdk version "23.0.2" 2025-01-21 OpenJDK Runtime Environment Corretto-23.0.2.7.1 (build 23.0.2+7-FR) OpenJDK 64-Bit Server VM Corretto-23.0.2.7.1 (build 23.0.2+7-FR, mixed mode, sharing)

That's on my Intel MacBook Pro. Pedestal's test suite loads a good amount of Java classes and Clojure namespaces.

To make things faster, there's ahead-of-time compilation (which basically captures the read-eval-create-bytecode part of loading a Clojure namespace as Java .class files that can be packaged into your app).

It's based on unification, like a Prolog. It does backtracking, like a Prolog. It has something akin to Prolog "cuts". It's equally not Markdown. What it is, is the sensibilities of Prolog (for parsing words into player intent - actions) and for implementing a world model, and for inferring facts from other facts ... and also the text-foremost sensibility of Markdown.

I love it. I've put considerable effort into building tools around it, and to building up a library of extensions.

That was my frustration as well; and it comes up in discussions. Simple to medium complex things are easy in Inform7, but when you start hitting the edges, you find out that its expressive English-like syntax is in the uncanny valley and exactly how you phrase things becomes vitally important and difficult to discern, even from the mounds of documentation it generates from your story file and from the standard library and extensions.

Clojure's slow, deliberate development pace confuses people. The core team takes backwards compatibility very seriously. What you see with each new Clojure release is generally improved performance, better Java interop, and a smattering of new features. This is doubly true for 1.12 which is doing quite a bit of invisible work to make interop considerably better.

So what you don't see is a constant flux of "innovation" followed by a community having to adapt to those innovations. People pull Clojure examples out of books that are 12 or more years old and they still run.

I think there's some very exciting things in the Clojure space, such as Clerk (https://github.com/nextjournal/clerk) for live notebooks, and Babashka (https://github.com/borkdude/babashka) for amazing (and amazingly fast) scripting.

Try Clojure 2 years ago

Nubank purchased Cognitect (the consulting company behind Clojure) in 2020, as well as Platformatec (employers of Jose Valim); the latter was for access to Platformatec's project management expertise (see https://building.nubank.com.br/tech-perspectives-behind-nuba... there is no Elixir code running at Nubank AFAIK.

There is Python on the "other side" of the ETL pipeline, but everything user-facing is (again AFAIK) Clojure on the backend and TypeScript in the Android and iOS apps.

In traditional databases, only the database engine has a scalable view of the data - that’s why you send SQL to it and stream back the response data set. With Datomic, the peer has the same level of read access as the transactor; it’s like the database comes to you.

In this read and update scenario, the peer will, at its leisure, read existing data and put together update data; some careful use of compare and set, or a custom transaction function, can ensure that the database has not changed between read and writes in such a way that the update is improper, when that is even a possibility - a rarity.

At scale, you want to minimize the amount of work the transactor must perform, since it so aggressively single threaded. Off loading work to the peer is amazingly effective.

It's actually quite a bit more than REBL; the description downplays something quite significant ... it can run as a client against a running Clojure in a separate process. Although that sounds simple ("it's just data") the number of edge cases involving everything from infinite lists, to dealing with functions as elements in the data, to differences in what's on the classpath between client and server, has impacted the implementation.

Babashka has unlocked so much power for me. Clojure and the JVM simply aren't set up for rapid start up, Babashka is instant. Bash scripting is alien, ugly, and hazard prone for me - Babashka makes it totally familiar, since it's virtually identical to Clojure. That is the definition of "have my cake and eat it too".

The end result is that anything the least bit non-trivial becomes a Babashka script, and I've created tools containing dozens (or potentially, hundreds) of sub-commands.

Joyful is the correct adjective.

In such a system, how does a reader find the root node? I'd be concerned about a) readers seeing a partial write to disk (or page buffer) during an update or b) a crash while the writer writes to disk.

I could imagine using two files, one containing the actual b-tree, the second containing the offset to the latest root note; the second file gets overwritten only after a successful write is verifiably written to disk.

Datomic's (https://www.datomic.com/) architecture is similar to this, but uses many write-once "segments" (which could be files in EFS or S3, or rows in DynamoDB or other stores).

Yes, and it caches results on the file system, so results persist between executions AFAIK. That comes in handy for one of my notebooks that has to slurp up Splunk data for 20 minutes.

I've been using Clerk quite a bit lately and found it quite useful. Although I'm normally in a REPL loop, having control over the output ends up being very useful, and the ability to paginate long results, or graph them, it's great.

I'm also using it for some Clojure mentoring/training, so providing my mentees with runnable, changeable examples to explore jumpstarts their understanding.

I wish I could share images easily here; one of my exercises is looking for a solution to a chess puzzle on a 4x4 board. The board state is a map, but with a little meta-data, Clerk will present it as a 4x4 chess board (that is, there's a function that renders the board map to HTML which is presented).

Under the covers, Clerk is doing a lot of work to analyze each code block in a notebook, to determine dependencies so that it can efficiently recalculate changes and control execution ordering.

Datomic is Free 3 years ago

Reverse engineering:

Some of the things that Clojure generates are valid bytecode for which there is no Java source code equivalent.

Reusing the pieces:

In general, Clojure works so well because it is all of a piece, with many decisions and subsystems working together. Datomic's source is the same way, you can't really consume just part of the elephant, even if you had the source code. Many things that Datomic does simply don't make sense at all out of context.

Datomic is Free 3 years ago

Although it is true that "time traveling" queries are relatively rare for production needs, the basic architecture supports things that many applications really need:

- It is possible to make queries against the database PLUS additional data not yet added, that is, "what if" queries

- Having a stable database-as-value is really useful for paginating results; you don't have to worry about new values being inserted into your results during execution, the way you do with traditional databases no longer how long (minutes, hours, even days) you take to traverse the data

- Reified transactions makes it possible to store extra data with each transaction, trivially, such as who made the update and why

- Immutability is amazing for caching at all layers

Datomic is Free 3 years ago

That would be very ugly source, as Datomic is written in Clojure and AOT compiled to Java bytecode. Due to the architecture of Clojure (especially, the use of macros) it is not exactly possible to work backwards from JVM bytecode to anything that looks like the original source code. It's not like Java where a clever decompiler can exploit output patterns generated by the Java compiler to make reasonable guesses at the structure of the source code.

But this is all besides the point; Datomic is now free (as in beer) with a great license (Apache 2.0). You can use this amazing tool for free, and you have as much need to look at the source to do so as you might need to look at PostgreSQL's source.

Some of us have been hoping for this day since Datomic was first announced, but even as an insider (I have been working at NuBank NA for less than a year) I was stunned at the speed with which this decision was made and implemented.

In a traditional database such as Postgres, queries are routed through a database server that handles both queries and updates, and is quite close to the storage. Reading excess data while executing a query is less of an issue as only the relevant data is eventually streamed to the client.

Datomic is quite different, in that peers (the other services that query Datomic data) execute the query engine library, and directly read the raw data from the store (or from a cache, as Datomic data is immutable). This design makes it easy to scale reads, but introduces the locality issues that the partitioning enhancement addresses.

Like any database, Datomic often needs to perform queries by reading data from its indexes. In fact, in Datomic, all data is stored in indexes - there aren't rows with indexes pointing to the rows, just indexes.

Datomic uses any of a number of stores, typically something like DynamoDB, to persist this index data.

When performing a query, Datomic peers read directly from the store (or from a cache) and read an entire block of index data as a single unit - each such block effectively contains many entity/attribute/value/transaction Datoms sorted by entity, attribute, or value, depending on which index is being scanned.

In other words, when reading all the attributes for entity X, Datomic will read from storage the block containing X, but also must inadvertently read other data that precedes or follows X.

The partitioning business is to ensure that related entities are stored close to each other; literally, a block of bits within the 64 bit entity id is set to the partition value. This ensures that the Datoms for related entities are stored close to each other, which in turn, ensures that the query can be satisfied using fewer of these index blocks, read from cache or storage, than if the entire entity id was arbitrary. Essentially, if entity X and entity Y are related, they can share a partition, and most likely will be stored together in a single index block.

This co-location of data can make a big difference in very large Datomic databases.

I wonder what the real story is here. When I left Walmart last June, the Portland office was already 100% remote, with no intention of reopening the office (it had been WeWork space before the pandemic). My team had been 100% remote since it was formed.

I’m pretty bad at the rumors game, but even so, by the time I left, it was clear to me that Walmart considered its IT future to be based in India.

Imagine the state of the world if Brendan Eich had known about Lua and spent those nine days integrating that into Netscape. I believe it had a compatible license, even back in 1995.