Porting Scala example to Clojure

31 views
Skip to first unread message

Stuart Halloway

unread,
Aug 25, 2008, 8:45:00 AM8/25/08
to clo...@googlegroups.com
For Java.next [1,2] part 3, I am porting Daniel Spiewak's color
example [3] to Clojure. Here's the Clojure version:

(defstruct color :red :green :blue)

(defn red [v] (struct color v 0 0))
(defn green [v] (struct color 0 v 0))
(defn blue [v] (struct color 0 0 v))

(defn keys-with-value-matching [map test-fn]
(for [pair (map identity map) :when (test-fn (last pair))]
(first pair)))

(defn basic-colors-in [color]
(keys-with-value-matching color (comp not zero?)))

(defmulti color-string basic-colors-in)
(defmethod color-string [:red] [color] (str "Red: " (:red color)))
(defmethod color-string [:green] [color] (str "Green: " (:green color)))
(defmethod color-string [:blue] [color] (str "Blue: " (:blue color)))
(defmethod color-string :default [color]
(str "Red: " (:red color) ", Green: " (:green color) ", Blue:
" (:blue color)))

I am reasonably happy with the struct and the defmulti, but I am sure
there is a more elegant way to do basic-colors-in and especially keys-
with-value-matching. What is a more idiomatic approach?

Thanks,
Stuart

[1] http://blog.thinkrelevance.com/2008/8/4/java-next-common-ground
[2] http://blog.thinkrelevance.com/2008/8/12/java-next-2-java-interop
[3] http://www.codecommit.com/blog/scala/scala-for-java-refugees-part-4

Chouser

unread,
Aug 25, 2008, 9:16:50 AM8/25/08
to clo...@googlegroups.com
On Mon, Aug 25, 2008 at 8:45 AM, Stuart Halloway
<stuart....@gmail.com> wrote:
>
> (defn keys-with-value-matching [map test-fn]
> (for [pair (map identity map) :when (test-fn (last pair))]
> (first pair)))
>
> (defn basic-colors-in [color]
> (keys-with-value-matching color (comp not zero?)))

I'm not sure why you used "(map identity map)" instead of just "map",
but other than that I think the only major idiom you're missing is
destructuring, which tidies this up a bit:

(defn basic-colors-in [color]
(for [[k v] color :when (not= v 0)] k))

Maybe others can spot more simplifications.

> (defmulti color-string basic-colors-in)

I don't feel qualified to comment on this use of multimethods, or
multimethods at all really. I simply wouldn't have thought of using
them in this case, but maybe it's fine. Maybe it's even great, I
don't know. :-)

--Chouser

Chouser

unread,
Aug 25, 2008, 9:38:34 AM8/25/08
to clo...@googlegroups.com
On Mon, Aug 25, 2008 at 9:16 AM, Chouser <cho...@gmail.com> wrote:
> On Mon, Aug 25, 2008 at 8:45 AM, Stuart Halloway
> <stuart....@gmail.com> wrote:
>>
>> (defn keys-with-value-matching [map test-fn]
>> (for [pair (map identity map) :when (test-fn (last pair))]
>> (first pair)))
>>
>> (defn basic-colors-in [color]
>> (keys-with-value-matching color (comp not zero?)))
>
> I'm not sure why you used "(map identity map)" instead of just "map",

Sorry to reply to myself, but immediately after sending I realized
(map identity map) isn't doing what I had thought it was, and since
others may be similarly confused...

You've already shadowed the builtin clojure/map function with your
function argument named map. So inside that function, it doesn't
matter whether you use the word "map" in the initial position or in
the last position -- both uses of the word "map" in (map identity map)
refer to your argument. In order to get to the builtin function you'd
have to say "clojure/map".

When I first read your code I assumed, probably partially because of
the use of "identity", that that expression was calling clojure/map.
But since it's not, how is it working? Let's say the hash-map {:foo
:bar} is passed in for "map". That means the expression in question
is the same as ({:foo :bar} identity {:foo :bar}). A hash-map in the
function position like that looks up it's first parameter ("identity"
in this case) in itself. Chances are the identity function wasn't
used as a key the hash-map, so the lookup will fail. Therefore the
lookup falls back to the default expression given in the next
argument, which in this case is the hash-map itself {:foo :bar}. And
thus we get the return value we were expecting, but for entirely
different reasons.

This also means there's a subtle bug lurking in the code. Not likely
to be a problem, I admit, but it would nonetheless be surprising:

user=> (color-string (assoc (struct color 20 30 10) identity {:red 99}))
"Red: 20"

By supplying a color struct that has an "identity" key, color-string
uses the key list from identity's value (that is, just :red), but the
value number from the original struct.

I don't know if that made any sense to anyone, but anyway...
--Chouser

Stuart Halloway

unread,
Aug 25, 2008, 9:52:27 AM8/25/08
to clo...@googlegroups.com
> (defn basic-colors-in [color]
> (for [[k v] color :when (not= v 0)] k))

Chouser: That is way better, thanks!

Stuart

Rich Hickey

unread,
Aug 25, 2008, 10:00:52 AM8/25/08
to Clojure
On Aug 25, 8:45 am, Stuart Halloway <stuart.hallo...@gmail.com> wrote:
> For Java.next [1,2] part 3, I am porting Daniel Spiewak's color
> example [3] to Clojure. Here's the Clojure version:
>
> (defstruct color :red :green :blue)
>
> (defn red [v] (struct color v 0 0))
> (defn green [v] (struct color 0 v 0))
> (defn blue [v] (struct color 0 0 v))
>
> (defn keys-with-value-matching [map test-fn]
> (for [pair (map identity map) :when (test-fn (last pair))]
> (first pair)))
>
> (defn basic-colors-in [color]
> (keys-with-value-matching color (comp not zero?)))
>
> (defmulti color-string basic-colors-in)
> (defmethod color-string [:red] [color] (str "Red: " (:red color)))
> (defmethod color-string [:green] [color] (str "Green: " (:green color)))
> (defmethod color-string [:blue] [color] (str "Blue: " (:blue color)))
> (defmethod color-string :default [color]
> (str "Red: " (:red color) ", Green: " (:green color) ", Blue:
> " (:blue color)))
>
> I am reasonably happy with the struct and the defmulti, but I am sure
> there is a more elegant way to do basic-colors-in and especially keys-
> with-value-matching. What is a more idiomatic approach?
>

It's a bit apples and oranges emulating pattern matching and case
classes with multimethods, but very illustrative. It's important to
note that the Scala code is using type tags, not values, in doing the
match, so I think using tags rather than looking for non-zero values
is more similar:

(defstruct color :red :green :blue)

(defn color-class [name r g b]
(assoc (struct color r g b) :tag name))

(defn red [v] (color-class :red v 0 0))
(defn green [v] (color-class :green 0 v 0))
(defn blue [v] (color-class :blue 0 0 v))

(defmulti color-string :tag)
(defmethod color-string :red [c] (str "Red: " (:red c)))
(defmethod color-string :green [c] (str "Green: " (:green c)))
(defmethod color-string :blue [c] (str "Blue: " (:blue c)))
(defmethod color-string :default [{:keys [red green blue]}]
(str "Color, R: " red ", G: " green ", B: " blue))

A critical difference, IMO, is that pattern matches are closed, and
multimethods are open:

(defn orange [r g] (color-class :orange r g 0))

(defmethod color-string :orange [{:keys [red green]}]
(str "Orange, R: " red ", G: " green))


Rich

Stuart Halloway

unread,
Aug 25, 2008, 11:27:00 AM8/25/08
to clo...@googlegroups.com
> It's a bit apples and oranges emulating pattern matching and case
> classes with multimethods, but very illustrative. It's important to
> note that the Scala code is using type tags, not values, in doing the
> match, so I think using tags rather than looking for non-zero values
> is more similar:

You're stealing my thunder. Using a type tag would be more similar,
but would introduce what I consider to be a bug in the Scala version:
(red 10) is not the same as (struct color 10 0 0).

> A critical difference, IMO, is that pattern matches are closed, and
> multimethods are open:

With Scala extractors, pattern matches can be open too. That said, I
find that writing the extractors to be syntactic vinegar compared to
multimethods. But Scala is a big beast, and I won't be surprised if I
find some tricks that make writing the extractors easier.

Stuart

Rich Hickey

unread,
Aug 25, 2008, 12:22:29 PM8/25/08
to Clojure


On Aug 25, 11:27 am, Stuart Halloway <stuart.hallo...@gmail.com>
wrote:
> > It's a bit apples and oranges emulating pattern matching and case
> > classes with multimethods, but very illustrative. It's important to
> > note that the Scala code is using type tags, not values, in doing the
> > match, so I think using tags rather than looking for non-zero values
> > is more similar:
>
> You're stealing my thunder.

Sorry, that wasn't my intent.

> Using a type tag would be more similar,
> but would introduce what I consider to be a bug in the Scala version:
> (red 10) is not the same as (struct color 10 0 0).
>

Ah, ok - well, take whatever you will from the my example, I guess I
really didn't get the original Scala example's purpose.

> > A critical difference, IMO, is that pattern matches are closed, and
> > multimethods are open:
>
> With Scala extractors, pattern matches can be open too.

Interesting. By that do you mean an existing match can work with
subsequently-defined types or that new 'cases' can effectively be
added later? Do you have a pointer to an example of the latter?

Rich

Stuart Halloway

unread,
Aug 25, 2008, 2:04:12 PM8/25/08
to clo...@googlegroups.com
> On Aug 25, 11:27 am, Stuart Halloway <stuart.hallo...@gmail.com>
> wrote:
>>> It's a bit apples and oranges emulating pattern matching and case
>>> classes with multimethods, but very illustrative. It's important to
>>> note that the Scala code is using type tags, not values, in doing
>>> the
>>> match, so I think using tags rather than looking for non-zero values
>>> is more similar:
>>
>> You're stealing my thunder.
>
> Sorry, that wasn't my intent.

Adding explicit smiley face: :-)

>>> A critical difference, IMO, is that pattern matches are closed, and
>>> multimethods are open:
>>
>> With Scala extractors, pattern matches can be open too.
>
> Interesting. By that do you mean an existing match can work with
> subsequently-defined types or that new 'cases' can effectively be
> added later? Do you have a pointer to an example of the latter?

The former is easy. It's not obvious to me how to do the latter, but
my instinct is that it could be done with some extractor trickery. If
I come up with an example I will post it.

Stuart

Reply all
Reply to author
Forward
0 new messages