Clojurdle: Wordle Written in ClojureScript
February 20, 2022 | 10 min. read
TL,DR: What it says on the box, play it here!
Inspiration
Like most people caught up in its viral success, my girfriend and I have been playing Wordle daily for the past few weeks. The core puzzle design of the game is great, but the social aspects are just as much fun; I doubt there's a greater joy in life than solving the day's word in two guesses, and being able to show all your friends (that are willing to listen, anyway).
As such, when it was revealed that the game had been sold to the New York Times for a price in the low seven figures, we were a little disappointed and predicted the imminent monetization of something we'd been enjoying for free. So, I embarked on a journey to steal fire from the gods and write my own version - in ClojureScript!
ClojureScript Per Se
I've been getting more into Clojure within the past year or so, and I've really enjoyed the experience. I've always been a big fan of functional programming, but Clojure is in a league of its own when it comes to both language design and developer experience. It supports the usual paradigm of designing your program as a series of transformations on data (rather than a set of imperative instructions), but the pervasive use of fundamental data structures rather than classes and interfaces keeps the language small and simple. The REPL has also ruined me as a programmer - I don't think I could go back to a language where I can't see the evaluated results of my code within my editor as I'm writing it. Other people have shilled Clojure far better than I ever could, so I'll just leave it at that.
ClojureScript, however, is something that I haven't dabbled with before this project. As a web developer, I've always had an ambivalent relationship with JavaScript. It's certainly not the most rock-solid Or ecosystem, for that matter. As thousands have said before, I think if JS had a little less churn in its community and a more robust standard library, it would do wonders for the language. but it's probably the best we've got when it comes to the browser. WebAssembly has been "almost here" for the past decade, and every framework-specific solution like Hotwire/ Livewire/ LiveView seems experimental at best (not to mention they use JS deep down anyway, with some extra framework lock-in to boot). As such, I was sceptical of ClojureScript.
Since ClojureScript is (for most intents and purposes) just Clojure compiled to
JavaScript rather than JVM bytecode with some added bells and whistles for the
DOM, there's a lot I like about it. The good ol' parentheses, Clojure data
structures, and higher-order functions like map
, filter
, reduce
, etc. are
all there. The REPL is there too, and one of the more useful features is the
ability to call js/
namespace functions within the REPL and see them run in
the browser. For example, evaluating (js/alert "hi there!")
within your
REPL-connected editor will cause the alert window to fire within your browser.
Pairing this with some clever use of defonce
and atom
, you can redefine
functions during development without having to do full page reloads or throw
away state during testing. Very cool!
Welcome To The Jungle
The Clojure ecosystem has been slowly moving towards
deps.edn and
tools.build, but
Leiningen is still a fair majority's build tool of
choice and has been the de-facto standard for the better part of a decade. I'm
not in love with Leiningen's "here's a massive
example
"-style documentation and deps.edn
is similarly
For example, if you run clj --help
, half the options just say something like "Use concatened aliases to
modify classpath...", which doesn't quite describe what the command does.
Similarly, the guide
has an example for "Using a main" that uses -X
. Wouldn't an example using -M
be more appropriate?
, but both are solid and there's a good consensus within the community on what
should be used.
ClojureScript is not so unified, however. There seems to be several dominant
project managers/build tools out there at the time of writing - should I depend
on NPM packages, or CLJSJS jars? Should I
use figwheel or
shadow-cljs? The Quick
Start has an example just using a
deps.edn
-like command, do I even need a third party tool? All this choice
leads to decision paralysis, or simply picking something and cargo-culting
because it's far too complex a decision to evaluate the entire ecosystem at
once. As for me, I just chose shadow-cljs because:
- It seemed the most similar to Leiningen, with a single
shadow-cljs.edn
file controlling all dependencies and configuration, - It manages both NPM dependencies and CLJS jars,
- It had the best documentation by far.
There is one library that's supremely popular and practically ubiquitous in ClojureScript, however, and that's Reagent. Reagent is a CLJS wrapper around ReactJS that honestly feels more-React-than-React. There's been a big push in the React world towards functional programming; your components and what they render should just be a pure function of their state and props. ClojureScript, being a functional language, fits this paradigm like a glove. Combined with Hiccup for homoiconic HTML, writing frontend user interfaces has never been easier.
Where There's a Word, There's a Way
Here is the bulk of the source for Clojurdle. As you can see, it's quite dense - I was able to get most features implemented in about ~130 I didn't get around to creating the "Share your score" feature, or tracking score distributions across days. I figure it'd just be myself and close friends/family playing, so there was no need to implement the social features.
You might also notice the :keyword.with.class.soup
everywhere,
and that's because I also decided to use TailwindCSS for styling. I chose
this because a) I suck at doing any kind of styling work without it, and b)
this was more of a project to learn CLJS rather than CSS. The class soup is
still pretty offensive, and I would not be surprised if the
:keyword.dot.class
syntax does not play nice with certain
Tailwind classes. It's definitely a tradeoff, though. Since the Hiccup
template that the component is returning is just code, being able to
reactively call CLJS to return different classes and their styling (as seen
here
) is really cool. The closest thing I can think of in mainstream JS land is
Vue's
:class
bindings, but (as with all things Vue) it's very hard
to tell when something is a string or when it's code. With CLJS the
distinction is irrelevant, since code is data and data is code.
The "word of the day" feature was also surprisingly easy to implement. I found a
word list online of common five-letter words and def
'd it in main.wordlist
to prevent cluttering up main.core
. I also decided to import my only other
dependency, cljs-time
, since doing date math with JavaScript's builtin Date
class seems like not a good time. The resulting
one-liner
to index into the word list based on the current day is pretty nice. I still
feel a little guilty about pulling in an extra dependency just for this one
line, though - if there's a nice way to do it in plain CLJS, please let me know!
That's a Wrap
I think that's all I can really say about the project. It was in total perhaps a weekend's worth of work spread over a few weeks, so a short and fun project. I couldn't call it an enormous learning experience since (as stated) CLJS is designed to be as close to Clojure as possible. I also can't say if I'll be universally reaching for CLJS for frontend projects from now on, but it'd be a frontrunner the next time I'm doing a Clojure web project.
Correction, Feb. 21 2022: Just as I was making this post public, I found that someone else in the Clojure community made a Wordle clone and posted it in the Clojurians Slack just a few days before this post. Here's the GitHub repo, and here's a pretty cool walkthrough of the project. That guy's version is also written in plain ClojureScript (other than shadow-cljs, no Reagent or anything), so I'd definitely check it out!