views:

124

answers:

3

I'm passing the name of a function for use in another method.

(defn mapper [m function]
  (cond
   (= '() m) '()
   true (cons (function (first m))
            (mapper (rest m) function))))

(println (map_ '((blue red)(green red)(white red)) #'first))

Is there a more idiomatic way to do this in clojure?

+3  A: 

I believe you got it mostly idiomatic. Clojure's own map uses:

(defn mapper [coll f]
 (when-let [s (seq coll)]
    (cons (f (first s)) (mapper (rest s) f))))

I have shortened it severely - the original produces a lazy sequence, deals with multiple collections, chunked-seqs, etc. By the way - I assume you want to pass the actual function, not it's name.

The coll and f are idiomatic arg names to represent collections and functions, respectively.

Rafał Dowgird
+3  A: 

Your version looks good to me. The usual names you will see in the clojure code base is 'coll' for collections. I have also seen 'xs' which is the Haskell style, I think. You may also refer to the Clojure library coding standards on various conventions.

Coming back to the example: Two observations.

  1. Use :else for 'cond' as the escape condition, instead of the Common Lisp style 'T'.
  2. Instead of assuming lists, think sequences.

With these two in mind, if I rewrite your code:

user> (defn mapper [coll f]
        (cond
          (not (seq coll)) nil
          :else (conj (mapper (next coll) f)
                      (f (first coll)))))
#'user/mapper
user> (mapper '(1 2 3) #(* % %))
(1 4 9)
user> (mapper [1 2 3] #(* % %))
(1 4 9)

Note that conj does the "right thing" as far as collections are concerned. It adds the new element to the head of a list, to the tail of a vector and so on. Also note the use of 'next' instead of the first/rest idioms in traditional lisp. 'next' returns a sequence of elements after the first element. So, empty-ness can be checked by seq'ing on the collection which will return nil for an empty list or an empty vector. This way it works for all collections.

Ramakrishnan Muthukrishnan
+4  A: 
  • Prefer vectors to lists. You don't have to quote a vector most of the time, and it has better performance for a lot of things, like random access. Lists are used much more rarely in Clojure than in other Lisps.
  • Prefer keywords to quoted symbols. Keywords stand out as "constant strings" or enumerated values. Keywords in Clojure can belong to a namespace, so they have all the advantages of symbols. And again, there's no need to quote keywords, which is nice. Quoted symbols are used pretty rarely in Clojure, unless you're writing macros.
  • #'first is the var called "first"; first is the value of the var called "first", i.e. the fn. In this case (#'first foo) and (first foo) give the same answer, but #'first does an extra dereference every time you call it. So don't do this unless you want that dereference to happen over and over. There's usually no need to use #'.
  • The built-in map is lazy, whereas yours isn't. The built-in map takes advantage of chunked seqs for better performance, whereas yours doesn't. Idiomatic code doesn't have to be lazy or use chunked seqs, but keep in mind that the builtins have some of this magic going on. So it's good to take advantage.
  • Rather than (= '() x), the idiomatic test for an empty seq is (seq x), which returns nil if x is empty. Note that in Clojure, (= '() nil) is false.
  • If you do ever need to use the empty list (which you should rarely need to do), you don't have to quote it. Just use ().
  • Built-in map takes the function argument first because it accepts multiple collection arguments. When a function takes multiple arguments, those arguments have to go last in the argument list. I think it reads better the other way too: "(map f coll): map this function across this collection".
  • There's no need to use cond if you only have two options. You can use if instead. And if one of the branches in your if returns nil, you can use when. It's nice to use when and if when appropriate, because they signal your intentions to the reader immediately, whereas cond could do anything and forces the reader to read more.

Rafał Dowgird's version is idiomatic, except I'd flip the order of arguments around. And I'd call it like this:

user> (mapper first [[:blue :red] [:green :red] [:white :red]])
(:blue :green :white)
Brian Carper
Rafał Dowgird
Great collection of tips.
edbond