Fork me on GitHub
#beginners
<
2017-02-28
>
donaldball02:02:12

Do other folks tend to frown at multimethods that aren’t open for extension?

tbaldridge02:02:35

How can you make a non open multi method?

donaldball03:02:19

E.g. one where the dispatch fn has a closed codomain

donaldball03:02:29

#{::success ::failure}

donaldball03:02:03

I find it occasionally useful when the case impls are long and don’t share much lexical scope, and I prefer it to e.g. process-success and process-failure because of the shared (tacit) type signature.

donaldball03:02:27

One of my coworkers opined that he always sees multimethods as a clear signal that something should be open for extension, which prompted me to wonder if that’s a widespread aesthetic. I feel like I’ve seen examples of both in the wild.

mathew.vijay03:02:37

Lisp Chronicles is a new blog that publish articles and tutorials on Lisp programming, with a slant towards Clojure and Scheme: https://lispchronicles.wordpress.com/

viveke12:02:43

I read through articles about mobile application development using clojurescript with react native and reagent . It is interesting. How do i need to proceed now ? Can anyone suggest ?

kyle_schmidt12:02:47

basic question but how can I update a value of a transient map?

moxaj12:02:48

@kyle_schmidt afaik, there's no transient equivalent of update, but you can use assoc!

kyle_schmidt12:02:45

If I have an atom (atom {}) I want to be able to update a key in that atom if it exists. If it doesn't then I want to give the key an initial value of 1

moxaj12:02:39

(swap! your-atom update :your-key (fn [value] (if-not value 1 (...))))

moxaj12:02:41

this will work if nil values are not allowed, otherwise you could use find

yonatanel13:02:43

@donaldball If ::success and ::failure are the only ever possible values then multimethods are way too open for this case and introduce more mental load. Maybe if you process a response from some API that's open for changes and might return ::pending in the future, then you have an excuse. In that case you will also have a :default implementation or you will get an exception. Having two separate functions (process-success, process-failure) is not so bad, depending on your case.

not-raspberry14:02:19

In cases like that I normally use a map for dispatch.

donaldball14:02:21

Me, I like the grouping that multimethods afford over separate fns. It’s immediately obvious that each does the same basic job, accepts the same types (contra the dispatch value, of course), and returns the same type. That is to say, I find they reduce mental load. But my aim is to inquire, not persuade, so I appreciate the perspective.

kyle_schmidt15:02:35

how can i use update-in of a vector of maps [{} {}] without referencing the map locations by index?

kyle_schmidt15:02:53

or is there a better way than update-in?

Alex Miller (Clojure team)15:02:25

maybe it should be a keyed map, not a vector

Alex Miller (Clojure team)15:02:54

when it’s hard to update your data structure, it’s a good time to consider if it’s the right data structure

kyle_schmidt15:02:59

the maps share the same keys 😕

Alex Miller (Clojure team)15:02:47

but presumably you want to update specific ones - how do you find which ones to update?

kyle_schmidt15:02:44

the keys are the same but the values are different I was looking them up on their values

kyle_schmidt15:02:01

is there a better way to represent this data?

Alex Miller (Clojure team)15:02:48

maybe those values should be keys in an outer map {1 {:id 1}, 2 {:id 2}}

Alex Miller (Clojure team)15:02:29

it’s exactly the same as making an index by key on a relational database table

kyle_schmidt16:02:52

Thank you @alexmiller Now I am trying to figure out why this is providing a null pointer exception in the math part of the last if-else block

(defn add-item [cart item]
  (swap! cart update-in [:items] (fn [value]
                                   (let [product (get value (:id item))]
                                     (if (nil? product)
                                       (assoc value (:id item) (assoc item :quantity 1 :quantity-price (:price item)))
                                       (do
                                         (update-in value [(:id item) :quantity] inc)
                                         (update-in value [(:id item) :quantity-price] (if (get item :bulkPricing)
                                                                                         (+  (* (quot (:quantity item) (get-in item [:bulkPricing :amount]))
                                                                                                (get-in item [:bulkPricing :totalPrice]))
                                                                                             (* (rem (:quantity item) (get-in item [:bulkPricing :amount]))
                                                                                                (:price item)))
                                                                                         (* (:price item) (:quantity item))))))))))

Alex Miller (Clojure team)16:02:16

I don’t have time to sort through all the details here but I would recommend breaking this into a handful of functions that you can test at a finer-grained level. every place you have update-in or fn here is an opportunity to make a function. those functions can be more easily tested/debugged.

curlyfry16:02:02

@kyle_schmidt I also noticed that the first line in your do block doesn't accomplish anything (`do` only returns the value of the last expression in the block, and the others are used for side-effects)

kyle_schmidt16:02:29

@curlyfry Thank you very much! That is helpful. How would I accomplish executing both sexps?

kyle_schmidt16:02:40

if I move the do up and perform multiple swap! that should work right?

curlyfry16:02:47

@kyle_schmidt I think it would be better to use a more functional approach. Try a simpler example, how would you add 1 to a number and then multiply it by 3?

rauh16:02:22

Common pattern is to do: (-> value (update-in [...]) (update-in [...))

curlyfry16:02:28

@rauh @kyle_schmidt Precisely, and in my simple example that would be either:

(defn foo [x]
  (* 3 (+ x 1)))
or as is more common for more complex updates:
(defn foo [x]
  (-> x
      (+ 1)
      (* 3)))

kyle_schmidt18:02:00

How can I update multiple keys of an atom in one transaction?

kyle_schmidt18:02:18

Something like

(defn add-item [cart item]
  (do
    (swap! cart update-in [:items] (fn [value]
                                     (let [product (get value (:id item))]
                                       (if (nil? product)
                                         (init-quantity-to-cart value item)
                                         (calc-product-quantity-price (update-quantity value item) item)))))
    (swap! cart assoc-in [:total] (reduce + (map  #(:quantity-price %) (vals (:items cart)))))))

rauh18:02:05

@kyle_schmidt You can refactor your firs (if (nil? ...)) when and instead use some defaults when you later get from the map (* (:price item) (:quantity item 1)) (note the 1)

rauh18:02:41

But answer your question: (swap cart (fn [cart] (-> cart (update-in ....) (assoc-in ...)))

rauh18:02:33

Also, I would call the function argument items instead of value.

kyle_schmidt19:02:43

(defn add-item [cart item]
  (swap! cart (fn [cart] (-> cart
                             (update-in [:items] (fn [value]
                                                   (let [product (get value (:id item))]
                                                     (if (nil? product)
                                                       (init-quantity-to-cart value item)
                                                       (calc-product-quantity-price (update-quantity value item) item)))))
                             (update-in [:total] (fn [_] (reduce + (map  #(:quantity-price %) (vals (:items cart))))))))))

rauh19:02:07

How does (:items cart) work when cart is an atom? Rename your outer variable, shadowing them is error prone

kyle_schmidt19:02:05

It returns a map

rauh19:02:20

Yes, you need to change the way you compute the totals to use the newly changed map

rauh19:02:02

oh that's right. I'm already getting confused because you have cart and cart, once being a map and once an atom.

kyle_schmidt19:02:26

I need to be able to use the updated map that is returned from the first update-in [:items] ...

Alex Miller (Clojure team)19:02:42

as an aside, I try to write code by focusing on the data structure and a set of functions around it and keeping that as separate as possible from the stateful operations. I make it a goal to have as few functions that touch the atom as possible (preferably none).

Alex Miller (Clojure team)19:02:13

here, I’d say add-item is the fn inside your current add-item

Alex Miller (Clojure team)19:02:30

it is the operation that adds an item to a cart (the data structure)

Alex Miller (Clojure team)19:02:44

wherever you invoke add-item, instead just call swap! there

Alex Miller (Clojure team)19:02:53

just some food for thought

rauh19:02:12

Yeah there is a few ways to do that. One is to just do (let [new-cart (....), new-total (....)] (assoc cart :total new-total)), but I'd refactor this (yet again).

rauh19:02:38

Let's say you want to write (swap! cart-atom (comp update-cart-total #(add-item-to-cart % item))

rauh19:02:57

Then you can keep those functions easy at what they do.

seancorfield19:02:57

Something to consider about add-item-to-cart is switching the arguments and making it curried so you can say (swap! cart-atom (comp update-cart-total (add-item-to-cart item))) Then the signature matching the name (defn add-item-to-cart [item cart] …) or (defn add-item-to-cart [item] (fn [cart] …)) for the curried version.

seancorfield19:02:30

(but since cart is more than just a sequence of items, there’s a good argument for keeping it as the first argument — “sequence goes last, collection goes first” and cart is a (specialized) concrete collection here)

seancorfield19:02:00

@alexmiller Do you have any insight on that? (argument order in the context of shopping cart and items)

Alex Miller (Clojure team)19:02:46

I’d make the collection first and use -> probably

seancorfield19:02:00

So (swap! cart-atom #(-> % (add-item-to-cart item) update-cart-total))? That reads better, I agree.

seancorfield19:02:16

(and therefore keep (defn add-item-to-cart [cart item] …))

Alex Miller (Clojure team)19:02:34

or even just (swap! cart-atom #(update-cart-total (add-item-to-cart % item))) (call me crazy)

Alex Miller (Clojure team)19:02:59

nested function calls bother me less the average Clojure dev from what I’ve found :)

Alex Miller (Clojure team)19:02:56

I rarely use comp other than when composing transducers

raspasov23:02:41

@alexmiller I agree, more of an “advanced” question I guess, but is there any performance (or other) difference between comp and nesting the functions “manually” the way you wrote it in the last example?

Alex Miller (Clojure team)23:02:11

comp is a function. Every function invocation takes a few ns to resolve the var. in reality, unlikely much different

Alex Miller (Clojure team)23:02:45

But as Rich would say, why guess when you can measure? Benchmark it and see