exo learns clojure because she hates slop

For a variety of reasons I won’t get into here, I do not want to use or depend on LLM-generated code if I can. In fact, the forum software we use right now has embraced LLM-generated code by including instructions for AI in the repository – in addition to all of the other reasons I dislike it. I’m interested in making something that could replace it.

But if I’m going to make something to replace it, I would like to use something that explicitly rejects LLM-generated code. This made me wonder what my favorite programming language, Haskell, thinks about LLM-generated code. I had a little trouble finding the contributing guidelines for the popular Haskell compiler GHC, but @Lain helped me out (source):

The GHC Project is generally willing to accept “AI Contributions”. These are contributions which are in whole or in part the product of AI tooling, with some caveats given the two primary challenges posed by AI output: correctness issues and potential licensing violations.

GHC has a bit of a nuanced opinion on the matter (which I do not agree with, but I digress), but it’s clear that they allow it. So, I was browsing the slopfree software index for things that have explicitly taken a position against slop in their contributing guidelines. Clojure takes a stance that is more aligned with my values (source):

Clojure’s code is written and reviewed by humans. Code generated by a large language model or similar technology, such as Anthropic’s Claude, GitHub/Microsoft’s Copilot, OpenAI’s ChatGPT, Facebook/Meta’s Code Llama et al, is not compliant with the covenants and representations of Clojure’s Contributor’s Agreement, and is thus not acceptable as code for Clojure.

And while the OpenJDK, the developers of the JVM that Clojure relies upon, has not taken an explicit stance, they do have verbiage in their contributors agreement requiring the code being contributed to be originally authored just like Clojure does. I’ve known about Clojure for a while and its overall design ethos vibes with me, so I’m going to try learning it. I’ll use this thread to report on my progress as I go along.

Although I am comfortable with functional programming, I haven’t used a Lisp-inspired language yet, so I’m sure it will be a bit fun to learn for the first time. I’m also interested if anyone else wants to learn it with me, or what other people think of Clojure, both good and bad.

4 Likes

I tend to fall into a more systems language camp, although I’m interested in functional languages. I do see that Zig is on the slopfree software index you mentioned though. I’m glad you shared that because I’ve had my eyes on Zig for quite a bit now.

1 Like

I just finished reading Aphyr’s Clojure from the ground up (or at least, all that is available right now). I found it a bit refreshing that the author took some time to reassure and support marginalized communities here and there, it was nice to see. I also found the information to be presented in a clear way, although I my eyes may have glazed over a little during the chapters about macros and polymorphism. :sweat_smile:

I’m a big fan of static type safety. One of the things I’m noticing about Clojure is that it’s dynamically typed instead of statically typed. I should have probably noticed this earlier, but this particular part from Aphyr’s tutorial makes it really clear:

If you’re coming from an object-oriented language (e.g. Ruby, Java), or a typed language with algebraic datatypes (e.g. Haskell, ML), you might see defprotocol, deftype, and defrecord, and think: “Ah, finally. Here are the tools I’ve been waiting for.” You might start by wanting to model a person, and immediately jump to (defrecord Person [name pronouns age]). While this is valid, you should take a step back […]

If you’re reaching for records for type safety: it’s not going to be as helpful as you’d like. Functions like assoc work equally well across all kinds of records, and the compiler won’t warn you about using the wrong keyword. Sticking to methods eliminates some of those risks, but it’s nothing like the type guardrails in Java or Haskell. Clojure programs generally rely more on tests and contracts to prevent these type errors. There are also static type systems like core.typed, which we’ll discuss later.

Plus, there are several parts in the tutorial where the dreaded null pointer exception has to be dealt with… by writing tests. I’m not a fan of that; I’d rather catch problems at compile time rather than at runtime or through tests.

That said, I think I have enough understanding to write some simple programs in Clojure now and I think it might be nicer to write small scripts in it compared to Haskell. I really enjoyed the chapter where a program was written to process FBI crime statistics data, as the code felt very expressive. Writing the same code in Haskell would have been a pain; I would have to make a data structure up front to contain all the data!

I have a Haskell program that I use to calculate durations for time tracking reasons, so I might try replicating that in Clojure for starters.

1 Like

I have this small Haskell program that I use to calculate durations between two times which I decided to replicate in Clojure for practice.

In Haskell, I use the attoparsec library to parse the input, but since Clojure has regex built-in, I decided to use that instead:

(ns scratch.time)

(require '[clojure.string :as str])

(defn parse-hm
  "Parses a string containing an hour and minute."
  [s]
  (->> s
    (re-matches #"([0-2]?\d):([0-5]?\d)")
    (rest)
    (mapv Integer/parseInt)
    (zipmap [:hour :minute])))

(defn parse-span
  "Parses a string containing a timespan between two times."
  [s]
  (->> s
    (#(str/split % #"\s*-\s*"))
    (mapv parse-hm)))

(defn parse-spans
  "Parses a string containing many timespans separated by commas."
  [s]
  (->> s
    (#(str/split % #"\s*,\s*"))
    (mapv parse-span)))

(defn calc-span
  "Returns the amount of time in a timespan."
  [s]
  (->> s
    (reverse)
    (apply merge-with -)
    (#(if (< (get % :minute) 0) (update % :hour dec) %))
    (#(if (< (get % :minute) 0) (update % :minute + 60) %))
    (#(if (< (get % :hour) 0) (update % :hour + 24) %))))

(defn calc-spans
  "Returns the total amount of time in multiple timespans."
  [s]
  (->> s
    (mapv calc-span)
    (apply merge-with +)
    (#(if (> (get % :minute) 60) (update % :hour + (quot (get % :minute) 60)) %))
    (#(if (> (get % :minute) 60) (update % :minute rem 60) %))))

(defn calc
  "Parses multiple timespans and returns the total amount of time."
  [s]
    (->> s
      (parse-spans)
      (calc-spans)))

There were a couple of things that I found rather frustrating while I was working on this.

  • I didn’t really enjoy working with macros. I would probably get used to this if I used the language more, but my lack of understanding of what macros do exactly meant that I often ended up with compilation errors I didn’t really understand at first. And it seems that macros are used for all sorts of relatively basic tasks, like defining anonymous function literals.
  • Not only is there no static typing, you can’t even define your own types. I mean, you can, but it’s rather cumbersome. You experience way less friction writing code if you don’t bother at all.

After this experience, I feel like Clojure could be a nice language to quickly write simple throwaway scripts, but I don’t like the idea of using it to write larger systems where I might want more maintainability or guarantees about how my code will act at runtime.

1 Like