Why Not To Use My Library clj-record
In 2008 I discovered Clojure, and I was extremely excited to finally have an easy entry-way to Lisp. Pretty early on I wanted to get my head around one of Lisp's killer features: macros. A little playing around in the REPL was easy but not satisfying: I wanted to build something powerful.
Having spent a few years doing Ruby on Rails, I thought ActiveRecord's
macro-like capabilities (has_many
and whatnot) would be a good goal. So I
built clj-record.
In retrospect, I went too far trying to build in Rails-like magic, so I want to point out the design flaws that I recommend you avoid in your own work and recommend that you choose something other than clj-record for your Clojure projects that use an RDBMS.
The API of clj-record is all in one macro: clj-record.core/init-model
.
It commits a few macro sins.
- It looks at the namespace from which you call it to find the name of your model.
- It intentionally captures a
db
var from your namespace. - It defs a bunch of fns in your namespace (most of which are just partially
applied versions of
clj-record.core
fns).
So you do this:
and that turns into something like this (leaving out the ns
):
This provided really good practice (and an example I've gone back to more than once) for quoting, syntax quoting, unquoting, and unquote-splicing.
But this sucks!
What init-model should do is return a data-structure that you then pass to clj-record's find, insert, and update fns. In addition to being simpler to understand than the hidden registry of model metadata that clj-record has to maintain, it would make it more natural to write generic, composable code, since the model itself would be a value.
If that were all init-model needed to do, the code (both inside clj-record and user code) would probably be even simpler if there were no macro at all. A set of functions could enhance the model data-structure as needed so that even model setup code could be composed like any other series of fn-calls.
Maybe it would be worthwhile having a macro that went something like this:
This macro would do a def
, but it's completely intuitive what that def is.
The third "sin" on my list above was about defining a bunch of fns in your
namespace. I call this a sin because it's not clear from reading the code
that there are a bunch of vars defined, making both discovery and debugging
more difficult.
There may be cases where "hidden defs" are justified by performance gains or
code reduction, but the fns defined by init-model don't pass that test
(because (clj-record.core/find-records widget attributes)
is no worse and
more easily composed with other generic model-processing fns than
(my.model.widget/find-records attributes)
).
If no one else were writing libraries, I'd try to remedy this in the next major revision of clj-record, but someone else has done this work (or something very much like it) already. Chris Granger wrote korma, and it offers similar functionality in a saner way. I'm also not using Clojure with a SQL database, so I wouldn't be able to eat my own dogfood.
If you're modeling entities in a relational database, I'd recommend
korma over clj-record. There may be even better options for you
(including just using clojure.java.jdbc
directly), so look around.
If you're writing macros, don't capture vars, don't make their functionality
depend on globals like *ns*
, and consider alternatives to creating vars
in the caller's namespace. While you're at it, consider whether the job would
be better done with plain old functions.
1 comment:
Thanks for posting this.
Post a Comment