Why have an idiom when you can have a function instead? Isn't that
the whole point of Lisp?
I've been thinking for a week. I think my previous map-utils proposal
is exactly the WRONG way to go. I'd like to propose an entirely new
technique, based on some of the work I've done recently.
I posted this in the main thread yesterday:
http://groups.google.com/group/clojure/browse_thread/thread/7bb01c235...
Let's use a visitor pattern instead. Way less code, way more
flexible. Way more future-proof.
I posted an example of predicate visitors yesterday. Here they are
again in terms of my visitor function:
;Works w/ predicate functions
(def keys-pred (visitor #(comp % key) (partial into {}))
(def vals-pred (visitor #(comp % val) (partial into {}))
Here's how the set of map functions would look.
(def keys-entry
(visitor
#(juxt (comp % key) val)
(partial into {})))
(def vals-entry
(visitor
#(juxt key (comp % val))
(partial into {})))
The merge case is a little more complicated:
(defn keys-entry-merge
"Like visit keys, but takes a merge function to resolve keys
collisions."
[merge-fn & args]
(apply (visitor
#(juxt (comp % key) val)
(comp
(partial apply merge-with merge-fn)
(partial map (p apply hash-map))))
args))
So, what's this look like in use? Let's start w/ predicate fns.
user=>(def test-map {"a" 1 "b" 2 "c" 3})
user=>(vals-pred filter odd? test-map)
{"a" 1 "c" 3}
user=>(vals-pred remove odd? test-map)
{"b" 2}
user=>(keys-pred filter #{"a"} test-map)
{"a" 1}
user=>(keys-pred remove #{"a"} test-map)
{"b" 2 "c" 3}
This also works with take-while/drop-while, the visitor pattern lets
you do that. I don't know WHY one would use them, but you could.
Here's how the entry visitors look
user=>(vals-entry map inc test-map)
{"a" 2 "b" 3 "c" 4}
user=>(keys-entry map #(.toUpperCase %) test-map)
{"A" 1 "B" 2 "C" 3}
And here's the merge case
user=>(keys-entry-merge + map (constantly "Example") test-map)
{"Example" 6}
I like that this doesn't actually produce any new sequence functions,
rather it decorates ones that already exist. I think it's a clear win
in the predicate versions. However, I'm not 100% sure about the entry
versions.
Can anyone think of a use case besides mapping operations?
Thanks,
Sean
On Nov 18, 10:36 am, Chouser <chou...@gmail.com> wrote:
> On Wed, Nov 18, 2009 at 12:55 AM, Sean Devlin <francoisdev
...@gmail.com> wrote:
> > map-vals
> > map-vals does come up on the list fairly frequently. I think this is
> > worth standardizing because it's a wheel people constantly re-invent,
> > and as such prone to error. To pick on your implementation in
> > particular (sorry)
> Heh, no need to apologize. Pick away.
> > (let [f identity]
> > (zipmap (keys coll) (map f (vals coll))))
> > I suspect this is a bug waiting to happen, because I don't think that
> > (keys coll) is guaranteed to return the arguments in the same order as
> > (vals coll).
> They will both return items in the same order as 'seq' would,
> which is all the same order. Hash maps make no guarantee about
> what that order *is* but it will be the same for the same
> immutable hash-map object. So that particular potential bug
> isn't there.
> The benefits of factoring out common code are well known and
> frequently discussed.
> But there are a couple benefits of *not* having fns to wrap small
> combinations of builtins like this:
> 1. People reading your code can need to be familiar with fewer
> fns in order to make sense of what you're doing. zipmap,
> key, map, and vals are all more commonly used and therefore
> more likely known to the reader than map-vals is likely to
> be.
> 2. If any of those builtins are not known to the reader, when
> they learn about them they will be learning more broadly
> useful functions. zipmap is useful in more different
> scenarios than map-vals, so taking the time to learn what it
> does will provide bigger bang for the buck.
> 3. When you're familiar with the builtins, the variety of ways
> they can be used, and some common idioms for using them, you
> have power to solve problems that are similar but slightly
> different. For example, say you want to map on both keys
> and values -- if you know the zipmap idiom above, the
> solution is obvious. If you only know about map-keys and
> map-vals you're likely to be driven to clumsier and less
> efficient combination of the two.
> So when writing my own helper functions, I try to weigh the
> benefits of each. In general the larger and more complex the
> repeated code, the more likely I am to favor factoring it out.
> But honestly I'm frequently pretty ambivalent about my
> conclusions, especially on these smallish functions.
> > trans/trans*
> > Before I get to involved, the name of these closures is negotiable.
> > That being said, let's go...
> > trans is designed to be used for adapting maps. The idea was to
> > combine it with destructuring to increase re-use, amongst other
> > things. It's common to have some type of fn like this:
> > (defn my-common-fn [{a :a b :b}] (* a b))
> > However, NONE of the half dozen data sources you query mention :a
> > or :b. They call it :pizza :barbeque or :dog :cat. Write enough
> > business software and you actually see this.
> Ok, now we're talking. I was writing code like this just
> a couple days ago. For business no less.
> > So, why does tran return a closure, instead of doing the work
> > directly? This has to do with how I use trans. It's usually in a
> > situation like:
> > * function composition, e.g. (comp my-common-fn
> > (trans :a :pizza :b :barbeque))
> > * an argument in map (map (trans :a :pizza ...) ...)
> > * an argument in my join library
> > Also, you may have noticed trans is variadic. Since it returns a
> > closure, I don't have to worry about where to place arguments in
> > partial.
> Hmmm... In my case I just wrote stuff like:
> (use '[clojure.set :only (rename-keys)])
> (-> input-map
> (rename-keys {:id :new-id, :person-name :name})
> (select-keys [:new-id :name :foo :bar]))
> It seems to me the 'count' and 'inc' of key "a" from the examples
> in your docs would fit into a -> form like this pretty nicely,
> without generating extra closures.
> On the other hand it would be wordier than your trans examples.
> I guess if you find 'trans' compelling I'll quit whining about
> it.
> > map-keys
> [snip]
> > It should be easy enough to reuse everything, but
> > you can't, because some XML library accidently capitalized
> > everything. Stupid XML.
> ok ok, you've convinced me. stupid xml.
> > map-keys to the rescue. For each hashmap you simply do the following
> > (map-keys (comp keyword #(.toLowerCase %) name) an-entry)
> To me, the merge-with case is the most compelling. If you don't
> need merge-with, zipmap will again work nicely and pretty
> succinctly:
> (zipmap (map (comp keyword #(.toLowerCase %) name) (keys x)) (vals x))
> But if you're going to have key collisions, zipmap's not going to
> cut it and you need to rework your whole expression to use
> merge-with instead. ...which is also a good reason to have one
> fn (map-keys) that can handle both cases without disruption.
> It might be worth calling out explicitly in the docstring that
> f applies to keys but merge-fn applies to values.
> --Chouser