7 ms·
Crafting Code in Clojure
- julian37 14y agoLisps are awesome! But to be fair, the Java code can be a lot more compact by using Guava: import com.google.common.base.Functions; import com.google.common.base.Joiner; import com.google.common.collect.FluentIterable; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; import java.util.Map; import java.util.Collection; public class MapUtils { public static String sortedKeyList(Map<?, ?> map1, Map<?, ?> map2) { Collection<String> sortedKeys = FluentIterable .from(Sets.union(map1.keySet(), map2.keySet())) .transform(Functions.toStringFunction()) .toSortedSet(Ordering.natural()); return sortedKeys.isEmpty() ? "<none>" : Joiner.on(", ").join(sortedKeys); } } Not nearly as pretty as the Clojure code, but a lot nicer than without Guava.
- deleted 14y ago[deleted]
- doktrin 14y agoThis is a good point. 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).
- zmmmmm 14y ago> that code could be made yet more compact if written in Groovy Indeed. It's very hard to read this and not think ... "but this would just be" in Groovy: [m1,m2]*.keySet().sum().sort().join(",")?:"<none>" So small that I would rarely even make a separate function for it. I imagine it's much the same in many dynamic languages.
- martinced 14y agoI 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.
- amalag 14y agoI like this better than the Clojure code.
- deleted 14y ago[deleted]
- fooooobar 14y agopackage com.howardlewisship; import java.util.*; public class MapUtils { public static String sortedKeyList(Map<?, ?> map1, Map<?, ?> map2) { Set<Object> allKeys = new HashSet<Object>(map1.keySet()); allKeys.addAll(map2.keySet()); return allKeys.isEmpty() ? "<none>" : org.apache.commons.lang.StringUtils.join(new TreeSet<String>(allKeys), ",").toString(); } }
- lispm 14y agoCommon Lisp (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<)))
- dizzystar 14y agoNice article! I have one question for others though: Am I the only one who disagrees with ->> being easier to read? From the OP: (->> (set (concat (keys map1) (keys map2))) (map str) sort) And also from the OP: (sort (map str (set (concat (keys map1) (keys map2))))) 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?
- lispm 14y agoI'm used to read Lisp indented: (sort (map str (set (concat (keys map1) (keys map2)))))
- bengillies 14y agoI'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).
- jshen 14y agoI'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.
- bfe 14y agoIt'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.
- robertfw 14y agoThanks 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.
- eckzow 14y agoThat'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 :))
- ims 14y agoThis is pretty pedantic, but wouldn't for k in m be more "pythonic" than for k in m.keys() ?
- robertfw 14y agoSomeone 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.
- lkrubner 14y agoClojure 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).
- mark_l_watson 14y agoStick 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.
- lkrubner 14y agoClojure 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).
- dxbydt 14y agodef f[T](a:Map[String,T],b:Map[String,T]) = { val res= (a.keys++b.keys).toList.sorted.mkString(",") if (res.length == 0) None else Some(res) } https://gist.github.com/4694911 https://gist.github.com/4694911
- aaroniba 14y agoThe equivalent clojure code would be: (defn f [a b] (let [res (join "," (sort (distinct (mapcat keys [a b]))))] (if (empty? res) "<none>" res))) Also I'm not a Scala expert, but I think you want .keySet instead of .keys, otherwise they won't be unique, right?
- dxbydt 14y ago> you want .keySet instead of .keys, otherwise they won't be unique, right? scala> val p = Map(1->4,5->3) scala> p.keys res0: Iterable[Int] = Set(1, 5) scala> p.keySet res1: scala.collection.immutable.Set[Int] = Set(1, 5) scala> p.keys++p.keys res2: Iterable[Int] = Set(1, 5) scala> p.keySet++p.keySet res3: scala.collection.immutable.Set[Int] = Set(1, 5)
- deleted 14y ago[deleted]
- deleted 14y ago[deleted]
- markov_twain 14y agoso this is horribly non-idiomatic ruby, but it was fun p ->(*_) { _.reduce({}, :merge).tap { |_| return "<none>" if _.empty? }.keys.uniq.map(&:to_s).sort.join(", ") }.({c:3,d:4,b:2}, {f:5,e:4,a:1}) p ->(*_) { _.reduce({}, :merge).tap { |_| return "<none>" if _.empty? }.keys.uniq.map(&:to_s).sort.join(", ") }.({}, {}) # "a, b, c, d, e, f" # "<none>" edit: a little bit better. func = ->(*_) { return "<none>" if _.all?(&:empty?) _.flat_map(&:keys).uniq.map(&:to_s).sort.join(", ") } func.({c:3,d:4,b:2}, {f:5,e:4,a:1}) #=> "a, b, c, d, e, f" func.({}, {}) #=> "<none>" https://gist.github.com/66b45e765a2aa6a97143 https://gist.github.com/66b45e765a2aa6a97143
- griffindy 14y agoslightly 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 https://gist.github.com/4695388 https://gist.github.com/4695388
- danneu 14y agoI 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.
- markov_twain 14y agowell, I hate to beat a dead horse, but here's a one-liner (ruby): (map1.keys | map2.keys).map(&:to_s).sort.join(", ").instance_eval { empty? ? "<none>" : self }
- 14y ago
- pjscott 14y agoBecause 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.
- jshen 14y agoI'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'
- cgag 14y agoIt 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.
- tel 14y agoGolfing it for Haskell 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
- evincarofautumn 14y agoSome 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 .:
- tel 14y agoNice! 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).
- enigmo 14y agoThat'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
- tel 14y agoBam! I didn't even think of taking advantage of the monoid.
- halter73 14y agoSince most other popular languages seem represented: string SortedKeyList<T, U>(params IDictionary<T, U>[] maps) { var keys = new SortedSet<string>(maps.SelectMany(m => m.Keys) .Select(k => k.ToString())); return keys.Any() ? String.Join(", ", keys) : "<none>"; } EDIT: I just realized that a lot of curly brace languages look the same to the uninitiated. The above is C#.
- shoo 14y agoSince someone had to do it, here's a version in brainfuck: https://github.com/fcostin/crafting_code_in_bf https://github.com/fcostin/crafting_code_in_bf Writing things in brainfuck is a good way to appreciate the things we sometimes take for granted, e.g. languages with more than 1 pointer.
- petercooper 14y agoA Ruby version that attempts to be as close to the elegant and readable Clojure solution as possible via similar 'crafting': def sorted_key_list(maps) maps.map(&:keys) .reduce(:&) .sort .tap { |a| a.push "<none>" if a.empty? } .join "," end Works out at 24 syntax-related punctuation symbols for Ruby, 20 for Clojure (ignoring the replace-empty function, admittedly).
- awj 14y agoYou 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
- petercooper 14y agoD'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 :-)