Java Allergies and Revisiting java.time

Wherein I argue that Clojurians should be a touch more embracing of Java interop and that java.time is actually good.

Introduction

I enjoyed Josh Glover’s post today in which he described how he queried his quickblog data with a dash of Babash…ka (sorry, couldn’t resist) to determine the number of posts he made during a certain date range—“in 50 simple steps”.

I appreciate this narrative, almost lab notebook, style of writing. It’s helpful to see how others work through problems, what tools they use to explore, and what alternatives they consider. It’s also humanizing, reminding us that elegant works we encounter likely didn’t emerge spontaneously, Athena-like, from the heads of their creators, but took time1 and, perhaps, toil. Ex nihilo nihil fit—the mill demands its grist.

It takes extra work to write as he did, tracking your own twists and turns, so, thank you to Josh for the post. (He has a good sense of humor and makes cool stuff, so check out his blog!)

I enjoyed seeing how simple the quickblog data model was along the way (universal Clojure plot spoiler: it was maps).

Scooby Doo Reveal meme of Clojure taking the mask off data to reveal a Clojure map beneath.

Fun Times with java.time

I’d like to borrow a few of Josh’s steps2 as a jumping off point to address date / time handling in Clojure3:

  1. Realise that :posts is a map of filename to post, but no matter!

    (->> opts :posts vals (map :date))
    ;; => 
    ("2022-08-26" "2022-06-22" ... "2022-07-02"))
    
  2. Sigh as you come to terms with the fact that you’re going to need to do some date parsing and you never remember how to use java.time and argh!

  3. Smile as you remember about tick, which is a library from our good friends at JUXT that provides a nicer API for this sort of stuff

I’ve had several discussions with peers who felt the exact same way about java.time. Hell, I felt that way for while.

But while I certainly agree that the JUXT folks are great friends to the community (💙), and that they’ve created something nice with tick, I’d also argue that java.time is pretty good, and it deserves another look if you wrote it off early on.

I can’t vouch for everything in java.time—I don’t know everything in java.time—but it was written (comparatively) recently, after the lessons of java.util.Date (bad) and Joda-Time (much better) had been absorbed, and they basically got it right.

I suspect most of us have dealt with a date parsing scenario like Josh encountered and found ourselves thinking: “just turn this damn string into a date—how hard could it be?” Maybe we even came from a language where a single function could parse "2022-08-26" and "2022-08-26T10:30" and "2022-08-26T10:30:00.000-05:00", and it all seemed so easy. And now we have this fussy, baroque java.time thing to contend with.

But the thing about those easy interfaces? They’re all bad, broken, or incorrect4. And, if you haven’t already, you will eventually get burned by them.

It’s actually trivial to demonstrate: what does "2022-08-26" represent? Is it August 26th, 2022 in your computer’s time zone? In GMT? Is it that date regardless of your timezone (meaning it’s not a single, unified point in time5)? These are all foundational concepts in our world; we need all of these concepts to have programs that do practical things, and since the semantics are not differentiated in the content of the string, they cannot possibly be handled correctly by a single (undifferentiated) parser.

Human representation of time is inherently complex (read Falsehoods programmers believe about time if you need convincing), so you really only have two options:

  1. The broken way — “Being small and simple [easy] is more important than being correct.” (perjoratively, “the Unix way”, à la The Unix Haters Handbook, pg. xx)
  2. The fussy way — “The design must be correct in all observable aspects. Incorrectness is simply not allowed.” (generously, “the Lisp way”, à la rpg).

A correct implementation, by necessity, reflects the inherent complexity of the problem domain6. And I think java.time actually does a good job of that. The core classes in java.time neatly represent the underlying concepts in the domain (and are immutable!)—it’s just that the domain is big, so there’s a lot for us to get our heads around.

But once we do:

  • A date, irrespective of time or timezone is just:
    (LocalDate/parse "2022-08-26")
  • If that includes a time, it’s:
    (LocalDateTime/parse "2022-08-26T10:30")
  • And a single point in time is:
    (Instant/parse "2022-08-26T10:30:00.000-05:00")

There’s really no fluff or inconsistency here—we just need to remember the class names. It wouldn’t be any easier if these were three function names instead.

But I’m Allergic to Java

Many Clojurians seem grumpy about seeing Java stuff in their Clojure code. I get it. I, too, came from the pure Lisp lands of kebabs and unified case (though preferably not *THE-SHOUTY-KIND*). And didn’t we do all of this to get away from that Java stuff?

Calling everything via functions looks nice, and has its benefits for porting (e.g. CLJS), and, hey, wouldn’t it be dreamy if one day we had a non-hosted, portable language with a spec and maybe it even bootstrapped itself, and compiled natively and… [trails off]

But in the meantime…

  • JVM interop is a freaking superpower. The presence of “Java stuff” in our language means we get to tap into thousands of person-years of engineering effort that our small community couldn’t have come up with on its own. It’s not a bug—it’s a feature! (But really this time.)
  • You’ll need to learn some Java stuff if you want to get good at Clojure. You need to be able to read stacktraces effectively, use records and protocols, and wield core Java libraries.
  • We shouldn’t shun interop in the same way that non-Lispers shouldn’t shun s-expressions. Think what that bias has cost them. If minor syntax details are your biggest concern, you need to work on harder problems!
  • Clojure 1.12 is around the corner and you’re going to be able to do this:
    (map LocalDate/parse ["2022-08-26" ,,,])
    Instead of this:
    (map (fn [v] (LocalDate/parse v)) ["2022-08-26" ,,,])
    Rejoice!

To Library Or Not To Library?

So we want to filter a sequence of maps based on their :date values—should we pull in a library? (You know what I’m going to say if you read all of the above, so I won’t belabor it…)

JUXT actually provides a nice guide to when you might want to use tick that makes thoughtful points you should read.

But for the basic cases, my fellow Clojurians, I really encourage you to just learn java.time. Because of the inherent complexity of time, it’s basically this set of classes or that set of functions—and the former is standard, built-in, battle-tested, and known.

Give it another chance.

It may not look like much, but it’s got it where it counts.

Postscript

Bonus tip: for super basic cases, you can just use read-instant-date (which pairs great with #inst’s convenient capabilities!):

(.before (clojure.instant/read-instant-date "2022-08-26") #inst "2022-09-01")
;; =>
true

It’s naive, without a proper understanding of timezones and such, but it handles the basics fine.


Thank you for reading, and thanks again to Josh for the great post.


  1. Hopefully of the hammock variety. ↩︎

  2. Slightly reformatted because computers↩︎

  3. It is my sincere hope that the author won’t mind my use of his post as a foundation to discuss this topic with the community; it’s certainly not intended as a critique of his post and / or code. ↩︎

  4. Just to be clear (and fair), I’m not including tick in my “bad” list—it’s actually a thin, sane layer over java.time (on the JVM). ↩︎

  5. If you want to have a bad time (ugh, sorry), try explaining to a user that a date basically isn’t a thing and that their globally-distributed team possibly should see two different dates on the same invoice depending on their location. ↩︎

  6. So many pigeons, so few holes. ↩︎