Belated impressions of Clojure

Clojure is over two years old, and only last week did I finally get round to writing something in it. Some reactions after writing a few hundred lines:

  • Error reporting is often a bane of new languages, but Clojure's is pretty good — usually you get a Java stack trace. However, I did get a number of NullPointerExceptions without stack traces.
  • I often made what must be a standard newbie mistake: using parentheses in place of square brackets. Some forms, like when-first, handle that well:
    user=> (when-first (x nil) 3)
    java.lang.IllegalArgumentException: when-first requires a vector for its
    binding (NO_SOURCE_FILE:0)
    But fn gives a singularly confusing error:
    user=> (fn (x) 2)
    java.lang.RuntimeException: java.lang.IllegalArgumentException: Don't know
    how to create ISeq from: clojure.lang.Symbol (NO_SOURCE_FILE:136)
    This could be easily fixed by checking the type in psig in fn's expander:
    (if (not (vector? params))
      (throw (java.lang.IllegalArgumentException.
              "fn requires a vector for its parameter list")))
  • Accessing nonexistent fields of struct-maps gives nil instead of an exception. This is consistent with other dictionaries, but it means this error isn't detected reliably, even dynamically.
  • doc is the first thing on the cheatsheet, but I didn't notice it until after I'd spent much of a day manually looking things up. :/
  • The documentation for proxy says it “expands to code which creates a instance of a proxy class that implements the named class/interface(s) by calling the supplied fns”, which sounds complicated and potentially inefficient. So I was very suspicious of it until I realized it's just inner classes.
  • Something like half of my total difficulties were about the Java libraries, not Clojure.
  • Java's GUI libraries are not entirely easy to use, but it's still wonderful to be able to take GUI support for granted. I'm used to GUI being possible in theory but not in practice.
  • for, doseq and range did exactly what I wanted, with no mental effort.
  • dosync took some getting used to. ref, deref and alter aren't unfamiliar, but having to announce in advance that you're doing state is a little unnerving. I didn't have any problems with code that might or might not be called in a transaction, but I was afraid I might. I'm not used to keeping track of this.
  • The inability to nest #(... % ...) is annoying. In about 200 nonblank noncomment lines, I used it five times. It would have been seven, but two of those would have been nested (immediately, in both cases) inside others. On the other hand, there were several other 1-argument fns that could have been written with #(... % ...), had I thought of it — which I didn't, because my pet language's equivalent only does partial application.
  • (alter r f x) is equivalent to (alter r #(f % x)). This saves a little typing, but it's such an arbitrary convenience that I found myself worrying about whether it worked with each function's argument order.
  • Clojure structures are dictionaries, but this isn't important; so far I've only used them like ordinary user-defined classes whose accessors happen to have names beginning with colons. It does mean they appear in a strange place in the documentation, though.
  • assoc is obvious in retrospect. I have wanted such an operator for structures, and I'd probably want it for dictionaries too, if I ever used nondestructive dictionaries.
  • Some functions I missed: abs, expt, union and intersection (they exist in clojure.set but not in the default namespace, and they don't work on lists), member? (on values, not keys), for-each (yeah, I know, I'm not supposed to want it), and a function that returns the first element of a list that satisfies a predicate.

5 comments:

  1. * The error you got in fn is because fn allows multiple arity bodies to be defined. What you wrote:

    (fn (x) 2)

    was interpreted as a multiple-arity version of

    (fn x)

    and because there was no body x was not interpreted as the optional function name… so it must be the arglist. Can't turn a Symbol into an ISeq.

    If you try

    (fn ((x) (* 2 x)))

    you'll get "can't turn PersistentList into PersistentVector" (or words to that effect).

    * struct-maps are not objects, they're a particular kind of map where some keys are always present (`contains?` returns true for those keys). If you want to find out if a map contains a certain key, use `contains?`. If you want to get the value of a key, with a default if it's not present, use `get`.

    * You can turn any sequence into a set by calling... `set`.

    * Returning the first element of a sequence that matches a predicate: either

    (first (filter p sequence))

    or

    (some #(and (p %) %) sequence)

    ReplyDelete
  2. An exception in Clojure always comes with a stack trace. However, the stack trace is not shown in the REPL by default; only the "toString" representation of the exception.

    However, the last exception is stored in *e, so you can retrieve the last exception with (.printStackTrace *e). I believe there are also functions in clojure.contrib.repl for nicely formatting the resulting stack trace.

    ReplyDelete
  3. I'm not sure what you mean by wanting member? on values, but assuming you mean values in a list/vector, the following is a common clojure idiom for checking membership:

    (some #{:bar} [:foo :bar :baz])

    (I don't have Cojure handy, so apologies if I mistyped this example in any way)

    ReplyDelete
  4. (fn (x) 2): Yeah, I figured out that it's interpreted as the multiple-arity-case form with a symbol as an arglist. That's why it makes sense to do the typecheck in psig, which processes a single arity case. (But maybe there are cases I don't know about, where a non-vector arglist is valid?)

    Struct maps work as maps, but that's not why they exist, is it? I see them as user-defined types that just happen to do slot access through IPersistentMap. User-defined types usually want to specify the set of keys exactly, not just impose a lower bound.

    Turning things into sets wouldn't have helped me much, because I wanted the result as a list. Maybe I could have converted back and forth, but that would have been inconvenient. A separate set type is a convenient thing, but set operations are also often useful on ordinary collections.

    Oh, filter is lazy! I forgot, since seqs print like lists. (I've been blissfully ignorant of which "lists" are really lists and which are seqs.)

    It's good to know stack traces are always available. In that case my complaint is limited to the non-obviousness of the error reporting for noobs like me who don't know about *e.

    By member? on values, I meant the set membership operator. Implementing it in terms of SOME by using another set as a predicate is a rather indirect way to get it.

    ReplyDelete
  5. ==
    Struct maps work as maps, but that's not why they exist, is it? I see them as user-defined types that just happen to do slot access through IPersistentMap. User-defined types usually want to specify the set of keys exactly, not just impose a lower bound.
    ==

    Unfortunately, your expectation does not match up with reality — struct-maps are an optimization strategy, little else. The docs make this explicit:

    ==

    Often many map instances have the same base set of keys, for instance when maps are used as structs or objects would be in other languages. StructMaps support this use case by efficiently sharing the key information, while also providing optional enhanced-performance accessors to those keys. StructMaps are in all ways maps, supporting the same set of functions, are interoperable with all other maps, and are persistently extensible (i.e. struct maps are not limited to their base keys). The only restriction is that you cannot dissociate a struct map from one of its base keys. A struct map will retain its base keys in order.

    ==

    You might want `deftype`.

    ReplyDelete

It's OK to comment on old posts.