More idiomatic way to use map like this?

237 views
Skip to first unread message

Steven Degutis

unread,
May 2, 2013, 5:21:46 PM5/2/13
to clo...@googlegroups.com
Given pseudo-code (Ruby-ish):

due = 100
cards = cards.map do |card|
card.applied_balance = max(0, due - card.balance)
due -= card.applied_balance

Notice how due changes at each turn, and each successive item in
"cards" sees the change.

What's an idiomatic way to do this in Clojure without using refs?

-Steven

Ben Wolfson

unread,
May 2, 2013, 5:30:56 PM5/2/13
to clo...@googlegroups.com
You can use reduce to carry forward both the new value on the cards, and the new value of "due".

user> (let [due 100
            cards [{:balance 120} {:balance 30} {:balance 35} {:balance 40}]]
        (reduce (fn [[due cards] card]
                    (let [applied (max 0 (- due (:balance card)))]
                      [(- due applied) (conj cards (assoc card :applied-balance applied))]))
                [due []]
                cards))
[30 [{:applied-balance 0, :balance 120} {:applied-balance 70, :balance 30} {:applied-balance 0, :balance 35} {:applied-balance 0, :balance 40}]]

The first item in the result is the remaining balance, the second is "mutated" cards.



--
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.





--
Ben Wolfson
"Human kind has used its intelligence to vary the flavour of drinks, which may be sweet, aromatic, fermented or spirit-based. ... Family and social life also offer numerous other occasions to consume drinks for pleasure." [Larousse, "Drink" entry]

Gavin Grover

unread,
May 2, 2013, 6:34:16 PM5/2/13
to clo...@googlegroups.com
Is this code applying an amount due against a customer's list of credit cards? If so, there seems to be a bug. The third line should be:

  card.appliedBalance = min(due, card.balance)

and the Clojure code I'd write is:

  (defrecord Card [balance applied-balance])

  (defn apply-due-to-cards [[due new-cards] card]
    (let [applied-bal (min due (:balance card))]
      [(- due applied-bal)
       (conj new-cards (->Card (:balance card) applied-bal))]))

  (assert (=
    (reduce apply-due-to-cards [100 []]
      [(->Card 10  0) (->Card 30  0) (->Card 150  0)])
    [0
      [(->Card 10 10) (->Card 30 30) (->Card 150 60)]]))

Also four lines long like the Ruby example, but it's easier to debug when there's a bug just by changing `reduce` to `reductions`. It's also threadsafe, and can be parallelized for large datasets by using the Clojure 5 Reducers library.

Steven Degutis

unread,
May 2, 2013, 11:34:41 PM5/2/13
to clo...@googlegroups.com
Epic win. Thanks guys, this is a really neat technique! I was thinking
the solution must be something like reduce but with two values, the
due amount and the cards, but I couldn't figure out how to get the
cards to "map". Turns out conj was the missing piece of the puzzle.
Thanks again!

Armando Blancas

unread,
May 2, 2013, 11:59:45 PM5/2/13
to clo...@googlegroups.com
This isn't idiomatic but can be useful for modeling mutable computations in pure functions:

(use '[blancas.morph core monads])
 
(def cards [{:balance 30} {:balance 25}])
(def due 100)
 
(run-state (mapm #(monad [due get-state
app (state (min due (:balance %)))
_ (put-state (- due app))]
(state (assoc % :applied-balance app))) cards) due)

mapm is a map for monads; (state x) boxes cards; the state is the total due.
;Pair([{:balance 30, :applied-balance 30} {:balance 25, :applied-balance 25}],45)

Jim - FooBar();

unread,
May 3, 2013, 9:28:17 AM5/3/13
to clo...@googlegroups.com
Not a Ruby expert here, but I think 'reduce' is your friend.... :)
loop/recur is also an option ...

the problem is not really how to loop, but how to replace all this
mutation...I'm saying this because you specifically asked not to use any
reference types.


Jim

Jim - FooBar();

unread,
May 3, 2013, 10:22:01 AM5/3/13
to clo...@googlegroups.com
something like this perhaps?

(loop [[c & more]] cards
res []
due 100]
(if-not c res
(recur more
(conj res (doto c (.setAppliedBalance (max 0 (- due )))))
(- due (.getBalance c)))) )

Jim

ps: haven't got a clue what objects you're working with so I'm
purposefully using setters which is close to what you showed in your
Ruby example....

Jim - FooBar();

unread,
May 3, 2013, 10:38:15 AM5/3/13
to clo...@googlegroups.com
oops there is a typo!

line 6 should be:
(conj res (doto c (.setAppliedBalance (max 0 (- due (.getBalance c))))))


On 03/05/13 15:22, Jim - FooBar(); wrote:
> something like this perhaps?
>
> (loop [[c & more]] cards
> res []
> due 100]
> (if-not c res
> (recur more
> (conj res (doto c (.setAppliedBalance (max 0 (- due )))))
> (->> c .getBalance (- due)))) )

Jim - FooBar();

unread,
May 3, 2013, 10:41:07 AM5/3/13
to clo...@googlegroups.com
I Just realised you've many responses and that you've already solved
your problem...sorry for the noise people.

Jim

Alan Thompson

unread,
May 3, 2013, 2:32:55 PM5/3/13
to clo...@googlegroups.com
Hey Armando - How did you get the nice syntax highlighting into your
post??? Enquiring minds wanna know.....
Alan

Cedric Greevey

unread,
May 3, 2013, 3:19:48 PM5/3/13
to clo...@googlegroups.com
On Fri, May 3, 2013 at 2:32 PM, Alan Thompson <thomps...@gmail.com> wrote:
Hey Armando - How did you get the nice syntax highlighting into your
post??? Enquiring minds wanna know.....
Alan

If the OP's problem is solved anyway, I suppose we can go OT a bit with this thread now.

AFAIK, it's done by sending the list an HTML email. I'm not sure what tools are used to generate the formatting, though, or of the netiquette of posting in HTML. Those posts render well in the Groups interface and in gmail, but for anyone using other mail clients to use the list, their mileage will vary. It probably looks like a horrendous mess to anyone using mutt, elm, or XFMail, for example. Are the majority of readers here using gmail or Groups, then?

(On a related note, there are a lot of top-posted replies and I'm probably guilty of a few of them myself. The gmail web interface makes it significantly more effort to bottom-post, whereas Google Groups makes it more effort to top-post, last time I checked out its interface. I haven't seen complaints here about either type of post, though, or about HTML mail to the list, so it seems as if all three are generally accepted here.)

Robert Pitts

unread,
May 3, 2013, 4:15:24 PM5/3/13
to clo...@googlegroups.com
Armando was a good citizen and sent along a plain-text version as well – https://groups.google.com/group/clojure/msg/6aae8287bc55d436?dmode=source&output=gplain&noredirect

Would still be nifty to know if there's an easy way to do this, Armando :)

Armando Blancas

unread,
May 3, 2013, 5:44:28 PM5/3/13
to clo...@googlegroups.com
Having failed many attempts, I asked Feng Shen and he kindly told me how: copy some formatted text off a browser and simply paste it on this editor box. So I made a gist and instead of putting this link https://gist.github.com/blancas/5507033 I just pasted the text.

Armando Blancas

unread,
May 3, 2013, 5:53:37 PM5/3/13
to clo...@googlegroups.com
On Friday, May 3, 2013 1:15:24 PM UTC-7, Robert Pitts wrote:
Armando was a good citizen and sent along a plain-text version as well – https://groups.google.com/group/clojure/msg/6aae8287bc55d436?dmode=source&output=gplain&noredirect

That must have been Google Groups doing the right thing... nice feature.

Timothy Baldridge

unread,
May 4, 2013, 1:52:59 AM5/4/13
to clo...@googlegroups.com
In general, loop/recur shouldn't be considered idiomatic, IMO. Instead, try for a more functional style:


due = 100
cards = cards.map do |card|
    card.applied_balance = max(0, due - card.balance)
    due -= card.applied_balance

becomes (untested):

(defn apply-balance-1 [{:keys [due] :as accum} [card-id balance]]
  (let [applied (max (- due balance))]
    (-> accum 
         (assoc-in [:applied card-id] applied)
         (assoc-in [:due] due))))


(reduce apply-balance-1
            {:due 100}
            {:card-id-1 4404.00
             :card-id-2 3020.00
             ....etc....})

Often I have found that using reduce forces me to break functions into several parts. If I used loop/recur, normally the function prelude, postlude and loop block are all smashed into a single function. With reduce + a do-step-1 function (as seen above) we can more easily reason about what is happening. The code is then easier to test as well, as we can test the calculations apart from the loop logic. 

When I'm performing Clojure code reviews, I often consider loop/recur to be a code smell. 

Timothy
         


--
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
 
 



--
“One of the main causes of the fall of the Roman Empire was that–lacking zero–they had no way to indicate successful termination of their C programs.”
(Robert Firth)

Alex Baranosky

unread,
May 4, 2013, 1:54:59 AM5/4/13
to clo...@googlegroups.com
I concur with Timothy's assessment.  Really well stated and illustrated use of reduce with a named reduce function.

Cedric Greevey

unread,
May 4, 2013, 3:43:22 PM5/4/13
to clo...@googlegroups.com
What about the times when one simply must use loop/recur for performance reasons? Although, one thought I had on that was to write a functional version, and if it's a performance bottleneck, write a loop/recur version and call the latter in performance-critical areas, but also have tests that check that both versions have the same semantics.

Devin Walters

unread,
May 4, 2013, 7:33:30 PM5/4/13
to clo...@googlegroups.com
I don't think that it's productive to discuss idiomatic code and performance in the same breath.

People do all sorts of nasty stuff when trying to squeeze performance juice out of their code. In my experience it's rare to see performance-related "idioms" beyond the obvious language-level constructs like type hints and indeed loop/recur. When serious performance concerns come into play, idioms are often broken via the use of clever tricks.

In the case of loop/recur I think I agree with Tim, Alex, and company.

What you're bringing up is a special case, so to your question I would respond with a question: What makes a context-specific performance-related concern idiomatic? My guess is very little.

In summary I would say that loop/recur is "idiomatic" when there is a serious, verifiable performance concern, but the overarching idiom is to prefer reduce as it's more consistent with Clojure's philosophy and design. Loop/recur is kind of like swearing. Reduce is typical conversation.

2c,
-- 
{:∂evin :√valters}

Reply all
Reply to author
Forward
0 new messages