Clojure is different from any other language I have ever used, and as I learned Clojure I had to struggle with the various programming habits that my mind has acquired over the last 14 years. Here were some of the main struggles:
1.) how do I live without a loop that lets me iterate some counters? The first Clojure (sort of) looping structure that I understood was (reduce), so for awhile I jumping through hoops to use (reduce) every time I wanted to go into a loop.
2.) how do I realize lazy sequences? I got myself badly stuck with some of the libraries, for instance Enlive, when I saved the nodes as a string and then later re-imported them to my code, at which point they were lazy sequences and I was wondering, how do I get this back to being Enlive nodes? Took me awhile.
3.) when do I divide code into a new file/namespace. In a language like Java the rule is something like "one class = one file" (I could qualify that, etc...). Clojure doesn't impose obvious dividing points on your code, and I think it takes a while to figure out what the right dividing points are (and I still wonder about what the best practice is, for instance: is it acceptable for code in one namespace to rely upon, and modify, an atom in a different namespace?)
Learning Clojure has been a fantastic educational experience for me. It is my favorite language and my favorite development eco-system. And I am lucky enough to be able to use at my job, so I get to work with it full time now, for which I am very grateful. (Previously I was getting a lot of corporate gigs where they needed PHP programmers who knew the Symfony framework -- I found those jobs very tedious, and, more to the point, after a few years I felt like I was not learning anything new).
Stick with it! I think that you will find Clojure + Compojure + Noir + Hiccup, etc. will be a refreshing break from PHP.
I was mostly using Ruby with Rails or Sinatra for a long time, and the switch to Clojure has been really good. Caveat: I have been actively using Lisp since 1982, and even so it took me a little while to switch over from Common Lisp. Keep at it, and I think Clojure will expand the way you think about programming.
I personally find the "code compactness" to be a bit of a red herring. Java's greatest weakness, after all, is not the verbosity per se. If all of its other failings were addressed I would probably scarcely care about the amount of code it takes to perform a given task.
Likewise, compactness is not what lies behind the current FP trend. Compactness, after all, while related to programming paradigms is more directly tied to the strength and level of abstraction afforded by a given library / API. Ruby's standard library, for instance, allows for some pretty insane one liners.
This is to say nothing of the development time overhead involved in switching paradigms (non-trivial, IMHO). This alone offsets "compactness", both from the standpoint of up front dev time as well as readability / correctness, for (IMO) up to half a year.
As an aside, that code could be made yet more compact if written in Groovy (Groovy-as-a-Java-superset and not functional Groovy, necessarily).
I think that what's important is not that this particular example has a short Guava equivalent but that, overall, Clojure code seems to be way shorter than equivalent Java code (even if you use Typed Clojure).
A lot of people are talking about a 10x code size reduction, which is amazing.
Also, it's not as if the Clojure code was some obfuscated brainf*ck-style unreadable one-liners. It's very easy to read for anyone remotely familiar with Clojure.
Because this is turning into a fun language code-off, here's something in Python:
', '.join(sorted(str(x) for x in set(map1) | set(map2))) or '<none>'
This works by making sets of the keys of map1 and map2, taking the union of those sets, converting each key to a string, sorting those strings, and joining them with ', '. And then if that generated an empty string -- if both dicts were empty -- then it evaluates to '<none>'.
Am I missing some difficulty here?
EDIT: and for an arbitrary number of maps, this one works:
import itertools
', '.join(sorted({str(x) for x in itertools.chain(*maps)})) or '<none>'
It iterates over all the keys of all the maps with itertools.chain(), converts everything to a string, adds everything to a set, sorts them, and then joins them with commas. I realize it's not a one-liner anymore because of the itertools import, but it's still pretty simple to follow.
I like Clojure, but for problems like this, it just doesn't seem to be doing quite as well as languages like Python.
I've never liked python code like this because I can't read it in left-to-right order nor right-to-left order. You have to read into the comprehension, then read left, then finally jump far right to the 'or'
It depends how you define better I suppose. I think they're both very readable, but I'm very willing to give up a little terseness for the benefits of a simpler syntax, immutable datastructures, and the kind of flexibility that lets you write things like -> to invert your control flow.
I'm so used to reading Lisp "in to out" that I have a difficult using the thread-fast macro. I guess it depends on your background. Do those who have have a bit of Lisp experience agree with me?
I'm a lisper and I prefer the macro. I have no idea if I'm in the minority or the majority. I have to admit that I've written more clojure than any other lisp, but I learned other lisps before I ever learned clojure.
I'm fairly new to Clojure, but am not new to bash (and its ilk), so tend to view the threading stuff as Clojure's version of bash's piping syntax (I realise they're not the same thing).
(defun example (&rest maps)
(format nil "~:[<none>~;~:*~{~A~^, ~}~]"
(sort (remove-duplicates
(loop for map in maps nconc
(loop for key being the hash-key of map collect key))
:test 'equal)
'string<)))
go :: (Show k, Eq k) => Map k v -> Map k v -> String
go m1 m2 = map show >>> sort >>> intersperse ", " >>> mconcat >>> orNone
$ keys m1 ++ keys m2
where orNone [] = "<none>"
orNone e = e
Some quick tips. “intersperse ", " >>> mconcat” is “intercalate ", "”, and I would prefer “.” to “>>>”. Even though left-to-right composition reads better to me, right-to-left is the style.
Gratuitious pointfree version (imports omitted):
go :: (Show k, Eq k) => Map k v -> Map k v -> String
go = orNone . intercalate ", " . sort . map show .: (++) `on` keys
where
orNone [] = "<none>"
orNone e = e
f .: g = (f .) . g
infixr 8 .:
Nice! I had (.) originally but reorganized it to compare better with the Clojure code. I really like (.:) and on though (which I'm always forgetting about).
That's missing a `nub` or something to get the unique keys. You can also combine the maps first and the results will come out sorted and unique already.... since we're golfing:
go :: (Show k, Ord k) => Map k v -> Map k v -> String
go m1 m2 = orNone . intercalate ", " . fmap show . keys $ m1 <> m2
where orNone "" = "<none>"
orNone x = x
It's great how he walks through the mental process of iterating his code, with examples at each step, and how it demonstrates the power of the language.
slightly more idiomatic, though perhaps not quite as clear as the clojure:
def unique_keys(*hashes)
result = {}
hashes.each do |hash|
result.merge! hash
end
return result.keys.map(&:to_s).sort.join(', ') unless result.empty?
'<none>'
end
I think accumulating an initialized variable in an imperative #each loop is decidedly unidiomatic, just something we tend to do until we really embrace things like #map, #partition, #select, #reduce, etc. (And what a glorious day that is!)
I reckon what makes his first example somewhat unidiomatic is the arbitrary lambda and the use & inner return from the #taps block.
His updated example is just good ol tacit Ruby. But then again the amount of times I see an #each block with inner logic used instead of a simple point-free one-liner in Ruby really just proves your point.
Thanks for this post! I'm diving into clojure (currently a python dev), and I'm starting to get that tingly anticipation of being able to use these kinds of tools. It was really useful to see how you iterate through your solution.
That's funny--as a python guy I was squirming for a set comprehension immediately:
>>> a = {'a':1,'b':2}
>>> b = {'b':3,'c':4}
>>> def keylist(*maps):
... return ', '.join(sorted({str(k) for m in maps for k in m.keys()})) or '<none>'
...
>>> keylist(a, b)
'a, b, c'
>>> keylist()
'<none>'
I'm sure people will have differing opinions on the readability of a nested set comprehension, but from the bullets at the top of the article to one-liner implementation in ~30 seconds. (For an arbitrary number of maps, no less.)
(edit: added '<none>' on empty, fixed formatting, whoops now I'm over 30 seconds :))
Someone pointed out elsewhere in the thread that this kind of code has to be read from the inside out, and for that reason I think the clojure code makes a better balance of readable vs succinct.
You actually want ".reduce(:|)", & does intersection on arrays. You can replace that .tap block with gsub abuse for a bit more golfing, and can replace the map/reduce combo with flat_map/uniq to eliminate one symbol-to-proc:
def sorted_key_list(maps)
maps.flat_map(&:keys)
.uniq
.sort
.join(",").gsub /^$/, "<none>"
end
D'oh, I misread the spec; this is where understanding the Clojure properly may have helped too. I thought the keys had to be in both maps, not the keys from both maps. Good call on the | and flat_map then :-)
1.) how do I live without a loop that lets me iterate some counters? The first Clojure (sort of) looping structure that I understood was (reduce), so for awhile I jumping through hoops to use (reduce) every time I wanted to go into a loop.
2.) how do I realize lazy sequences? I got myself badly stuck with some of the libraries, for instance Enlive, when I saved the nodes as a string and then later re-imported them to my code, at which point they were lazy sequences and I was wondering, how do I get this back to being Enlive nodes? Took me awhile.
3.) when do I divide code into a new file/namespace. In a language like Java the rule is something like "one class = one file" (I could qualify that, etc...). Clojure doesn't impose obvious dividing points on your code, and I think it takes a while to figure out what the right dividing points are (and I still wonder about what the best practice is, for instance: is it acceptable for code in one namespace to rely upon, and modify, an atom in a different namespace?)
Learning Clojure has been a fantastic educational experience for me. It is my favorite language and my favorite development eco-system. And I am lucky enough to be able to use at my job, so I get to work with it full time now, for which I am very grateful. (Previously I was getting a lot of corporate gigs where they needed PHP programmers who knew the Symfony framework -- I found those jobs very tedious, and, more to the point, after a few years I felt like I was not learning anything new).