Hi Jon,
You've put it out there, so I'm guessing you'd like a critique? Of course, some of what I say is colored with my own preferences, but I should have a couple of useful things to say. If I'm lucky, someone may even critique my critique. :-)
I'll reference your code by line number, so I don't have to bring it all in. For convenience, it's at:
Starting with declares on lines 3-5, the declare macro is very similar to def. An equivalent would be to say:
(def parse)
(def parse-array)
(def parse-hash)
However, declare has the bonus that you can say it all in one line:
(declare parse parse-array parse-hash)
This would expand to:
(do (def parse) (def parse-array) (def parse-hash))
So if you're using declare then I suggest collapsing it all into a single line (and if you want multi lines, then use def). That said, parse-array and parse-hash are only referenced in one place, so I would have moved then above the definition of parse-map thereby avoiding the need to pre-declare them.
When you define parse-map on line 7 you use the hash-map function. This function is useful to construct things programmatically, but when you're defining it statically like this then it is typical to use the literal form of {}.
Also, you've keyed parse-map on strings. While that is often useful, in this case tnetstrings is explicitly defined to use single characters, so you can take advantage of it here. Notice how on line 41 you need to calculate both the start and end of the string? If you're selecting a single character then you can just say nth to get the character at the given location, and you don't need to do the second addition (this is the same as .charAt, but takes advantage of java.lang.String being a seq).
Finally, in the map you've correctly wrapped java.lang parsing functions (which needed wrapping because they don't implement clojure.lang.IFn, Callable or Runnable), but you don't have to wrap any of the functions that are already in Clojure. Yes, it works, but it's completely redundant. So instead of:
"]" #(parse-array %)
"}" #(parse-hash %)
You can have:
"]" parse-array
"}" parse-hash
You could also say:
"," str
But this will just call str with a string, which returns an identical string. Instead, you can just have a function that returns the argument that it's given. This is already defined as identity so this makes much more sense to use instead of str.
parse-array on line 14 is mostly OK, though there are a few things. A minor point is that you can take advantage of strings being sequences by calling count instead of .length (my own code was using .substring instead of subs, so I can't talk). The main thing to comment on here is that you've wrapped the entire function in an if to provide termination of recursion. This looks so much like a loop/recur body that it just seems to be crying out for it (though that's probably because I've used loop/recur a number of times and tend to see it).
I should also complement you on using into, not because it's anything particularly special, but because I always forget to. :-) I tend to use a reduce/conj combo, which works but is not idiomatic. I think it betrays how I tend to think about certain types of operations.
Getting to parse-hash the first thing I noticed was that it lines 25-27 are identical to parse-array. That's almost always an indication that there's a more general operation going on. Lines 28-30 are practically identical as well: the only difference is that they include an offset. So now you've done the same operation 3 times, twice in the same function. You've already seen my approach to this, so I don't have to spell it out, but this is exactly the kind of thing you want to be on the lookout for.
Lines 31-33 have a couple of things going on. To start with, unless you have a specific reason to need a sorted-map, you shouldn't create one explicitly. It also seems strange that you chose to use one when you used a hash-map back on line 7. Again, it makes a lot more sense to create a map using the literal syntax {}. You correctly used literal syntax for a vector back on line 20, so even if it weren't more idiomatic it would make you more consistent.
The other strange thing is the use of conj to join a map to a map. The conj function takes a collection first, and an "item" second. In your case you've put a map in the item position, but an "item" for a map should be a key/value pair. ie. The following is a typical way to use conj on a map:
(conj {"k1" "v1", "k2" "v2"} ["k3" "v3"])
That said.... I checked it out and your usage works. I was surprised to learn this. All the same, if I wanted to add a map to a map then I'd use into. If I didn't have into I've have used reduce/conj to insert the items one key/value at a time. That would be the more consistent approach.
I was curious about it, and so looked at the implementation of conj. This is just cons, with the arguments reversed (again, I wouldn't have thought it would work). So I looked at the implementation of cons in src/jvm/clojure/lang/APersistentMap.java (I was looking at the Clojure 1.4 sources). Sure enough, this checks to see if the item being cons'ed is a Map.Entry or a 2 element vector, and adds them as a key/value. But otherwise it treats the argument as a seq of Map.Entry, and inserts everything: which is the behavior you've shown. So I learnt something new. :-)
Finally, we get to the parse function on line 35. Other than the comment I've already made about searching for a character instead of a string (as the key to parse-map on line 7) then there is nothing to comment on here.
One final question... if the string contains more data, then you'll just ignore it. This is safe, but perhaps it would be useful to see what wasn't parsed? If there was some reason to look at this data, then how do you think you might go about it? I ask this because my own code returned the unparsed data along with the parsed structure, and I noticed that the reference code on the tnetstrings site does the same. Returning both had the additional benefit of letting recursion do more of the work for me when parsing arrays and maps.
Regards,
Paul