Fork me on GitHub
#beginners
<
2021-04-14
>
Prabu Rajan05:04:14

Hello, I am new to clojure coming from the Java and Javascript world. I think its a beautiful, expressive language and I am loving learning it. I am building a small headless restaurant app. My data is : menu ( a map of id -> { name, price, quantity } ), orders (a map of id -> [ { name, quantity } ] ). Both menu and orders are atoms I am trying to write a function that takes in a list of order-items and creates an order. So, I need to deduct the ordered quantity from the overall inventory from the menu map and then create an order. Below is my implementation. I see that only the orders atom gets updated, and the menu atom isnt. However, if I separate the swap atom code for menu into a separate function, it works. Please help pointing out what is wrong

(def orders (atom {}))
(def menu (atom {}))

(defn create-order [{:keys [order-items]}]
  {:pre [(s/assert ::order-items order-items)]}
  (for [item order-items]
      (swap! menu update-in
             [(keyword (slugify (:name item))) :quantity]
             #(- % (:quantity item))))
  (swap! orders
         assoc
         (keyword (str (UUID/randomUUID))) order-items))

(create-order {:order-items [{::name "chilly parotta"
                              ::quantity 30}
                             {::name     "mutton biriyani"
                              ::quantity 3}
                             {::name     "masal dosa"
                              ::quantity 1}]})

seancorfield05:04:27

@prabu.rajan for is lazy. Functions evaluate each expression but only return the last expression. So a lazy expression is only “evaluated” at the top level, effectively.

seancorfield05:04:48

(so your for doesn’t get realized so it doesn’t do anything)

seancorfield05:04:55

In general, you should not mix laziness with side-effects so you should use doseq (imperative) here rather than for (which produces a lazy sequence).

seancorfield05:04:18

Also, you should try to write your functions as pure functions that don’t have side-effects where you can.

seancorfield05:04:00

In this case, create-order could be passed the current menu and orders values and return the new values of both, for the caller to keep track of.

seancorfield05:04:15

(defn create-order [orders menu {:keys [order-items]}]
  [(assoc orders (keyword (str (UUID/randomUUID))))
   (reduce (fn [m item]
             (update-in m [(keyword (slugify (:name item))) :quantity] #(- % (:quantity item))))
             menu
             order-items)])
;; and then:
(let [[new-orders new-menu] (create-order orders menu {:order-items [...]})]
  (println new-orders)
  (println menu))
and each time you call create-orders with the current orders and menu values you get a pair of new values back to use in the next call.

piyer16:04:25

@prabu.rajan I would consider creating order and doing a dec on the inventory to be one atomic operation.

Prabu Rajan16:04:03

Thanks @UGJ2DMT2S you say both orders and menu (inventory) should be maps in the same atom in terms of data structure?

piyer16:04:01

I was advocating for using ref when you perform the transaction.

Prabu Rajan16:04:37

I am a beginner, and just started learning for 2 weeks now. Are you talking about this? - https://clojure.org/reference/refs

seancorfield17:04:33

ref usage is very, very rare in the wild. A lot of programs can work just fine with a single atom of their state if they really need mutable data at all.

seancorfield17:04:04

The nice thing about writing your function as a pure transformation of input to output is that you can usually adapt it for use with swap! on a single atom.

seancorfield17:04:17

In this case, you could have (atom {:menu {...} :orders {}) and then #(let [[orders menu] (create-order (:orders %) (:menu %) %2) {:orders orders :menu menu}) could be your adapter:

(swap! restaurant #(let ..) {:order-items [..]})
(off the top of my head)

seancorfield17:04:04

(or just rewrite create-order a bit to accept the restaurant hash map as input — first argument — and return an updated hash map)

Prabu Rajan20:04:07

Thanks @seancorfield makes sense. I like the idea of minimizing code that deal with side effects as much as possible. I was mainly using atoms here, since that seems to be the clojure idiomatic way to deal with mutable state and this is a small single module app anyways. In larger apps, there might be better ways to handle mutable state like caches? I haven’t gone to that extent so far

seancorfield20:04:24

https://github.com/clojure/core.cache — expects to be used with an atom for each cache. The clojure.core.cache.wrapped namespace is the easiest/safest way to use it.

seancorfield20:04:02

For a reference point, here’s some stats about our codebase at work:

Clojure build/config 19 files 224 total loc
Clojure source 350 files 89404 total loc,
    3579 fns, 904 of which are private,
    574 vars, 30 macros, 92 atoms,
    858 specs, 33 function specs.
Clojure tests 379 files 23537 total loc,
    4 specs, 1 function specs.
We have no ref’s at all. We have 136 agent’s, which are mostly used to track metrics that we report via New Relic.

seancorfield20:04:56

(we have another ~50 atom’s in our test code which we don’t bother to report in our stats)

Prabu Rajan21:04:17

Great! I was going through the usages and the articles related to this library. Will try to put that to use soon in my next web app REST APIs

seancorfield05:04:12

(and, FWIW, I miss dosas… a local restaurant had a southern chef for a while and he had a variety of dosas on the menu, but he left and they haven’t had much southern food for a while 😞 )

🙂 3
Prabu Rajan05:04:51

Sad to hear and wishing you get to eat your favourite dosas soon!

Prabu Rajan05:04:14

Thanks a lot @seancorfield, really helpful. As you can see, it takes a lot to unlearn the imperative Java / Javascript way of coding and learn the new clojure functional way! but I hope I will get better at it over time!

seancorfield05:04:12

(that code isn’t quite right but should give you some guidance… I’m now trying to get it right in the REPL!)

Prabu Rajan05:04:37

You are so helpful, I think you shouldn’t worry since I got the idea. Let me swim the river and learn to swim 🙂

seancorfield05:04:58

dev=> (defn slugify [s] (str/replace s " " "-"))
#'dev/slugify
dev=> (def orders {})
#'dev/orders
dev=> (def menu {:chana-masala {:quantity 2} :paneer-bhurji {:quantity 1}})
#'dev/menu
dev=> (defn create-order [orders menu {:keys [order-items]}]
 #_=>   [(assoc orders (keyword (str (UUID/randomUUID))) order-items)
 #_=>    (reduce (fn [m item]
 #_=>              (update-in m [(keyword (slugify (:name item))) :quantity] #(- % (:quantity item))))
 #_=>              menu
 #_=>              order-items)])
#'dev/create-order
dev=> (let [[new-orders new-menu] (create-order orders menu {:order-items [{:name "paneer bhurji" :quantity 1}]})]
 #_=>   (println new-orders)
 #_=>   (println new-menu))
{:f3666dfc-a9f2-4432-8979-bdffedb83b80 [{:name paneer bhurji, :quantity 1}]}
{:chana-masala {:quantity 2}, :paneer-bhurji {:quantity 0}}
nil
dev=> 

seancorfield05:04:16

(this is making me hungry!)

Prabu Rajan05:04:27

wow! great, thanks! looks like you are a huge fan of indian food!

seancorfield05:04:34

Born and raised in the UK — it’s the national dish 🙂 Now I live in the San Francisco Bay Area and good Indian food is hard to find. I’m lucky to have a fairly decent restaurant fairly locally but I’ve mostly been very disappointed with Indian restaurants in America.

Prabu Rajan05:04:51

Oh nice! I live in the SF bay area too. Having moved from south India to the bay area just 3 years back, I absolutely echo your sentiments about food! That is the biggest thing I miss about india here!

seancorfield06:04:40

If you have any local recommendations, I’m all ears! 🙂

seancorfield17:04:54

Thank you! A couple of those are a bit far away for me — I’m in Castro Valley in the East Bay — but Dosa Hut is pretty close and I see there’s another Saravanaa Bhavan nearby at 3720 Mowry Ave, Fremont, CA 94538.

Prabu Rajan21:04:34

oh ok. I stay in Fremont. Yes, you can try the Mowry Avenue Saravana Bhavan, but their sambar and chutneys have lost their touch, but dosas are still good.

3
rafalw07:04:48

Hi, I'm looking at https://github.com/redstarssystems/context/blob/master/src/org/rssys/context/core.clj What is that convention to prefix var with *, for example *ctx ?

introom13:04:00

hi. What syntax is this, the #: part.

(let [m #:domain{:a 1, :b 2}
      {:domain/keys [a b]} m]           
  [a b])                                ;  1, 2

Tomas Brejla13:04:52

basically just a different (shorter, easier to read/write) representation of map containing namespaced keys

user=> {:domain/a 1, :domain/b 2}
#:domain{:a 1, :b 2}
It can only be applied if all the ("top-level") keys in that map share the same namespace. If you use more than one key namespace, this form cannot be applied
user=> {:domain/a 1, :domain/b 2, :anotherdomain/a 3}
{:domain/a 1, :domain/b 2, :anotherdomain/a 3}

Tomas Brejla13:04:41

documentation: https://clojure.org/reference/reader see this part

Map namespace syntax
Added in Clojure 1.9

Tomas Brejla13:04:58

btw it's also possible to use #:: form, didn't know about that :thumbsup:

user=> #::{:some-key "some value"}
#:user{:some-key "some value"}

🆒 3
Tomas Brejla13:04:53

btw there's also a nice page with description of "weird characters", very handy https://clojure.org/guides/weird_characters

introom14:04:55

Yes. Thanks

Eric Ihli13:04:27

I have a custom data type that I want to print in the REPL as if it were an IPersistentMap. It implements everything needed to print as an IPersistentMap but it doesn't implement everything to act as an IPersistentMap. How can I do that? I'm looking over https://github.com/clojure/clojure/blob/master/src/clj/clojure/core_print.clj don't see an obvious way.

ghadi13:04:54

link/paste what you have so far and describe how it is not acting as a map there are a lot of interfaces and methods to implement to get it to work well

ghadi14:04:22

(core_print will not be helpful as it precedes the definition of deftype/reify during clojure's load)

Eric Ihli14:04:36

https://github.com/eihli/clj-tightly-packed-trie/blob/main/src/com/owoga/tightly_packed_trie.clj It's ILookup. I want the seq of key/values to print. But it's "read-only". Can't assoc into it or empty it or anything.

ghadi14:04:21

oh I thought you said it was printing

Eric Ihli14:04:33

Perhaps I'm using the wrong terms. When I evaluate the TightlyPackedTrie in my editor, I see:

;; => #object[com.owoga.tightly_packed_trie.TightlyPackedTrie 0x21f76d8d "[email protected]"]
I want to see (which is what I see with the same data as a https://github.com/eihli/clj-tightly-packed-trie/blob/main/src/com/owoga/trie.clj#L57https://github.com/eihli/clj-tightly-packed-trie/blob/main/src/com/owoga/trie.clj#L57https://github.com/eihli/clj-tightly-packed-trie/blob/main/src/com/owoga/trie.clj#L57):
;; => {(1 2) 12, (1 3) 13, (1) 1}

ghadi14:04:15

(deftype Foo [a b c]....)
;;
(defmethod print-method Foo [my-foo ^java.io.Writer w]
  ... my custom foo printing method)

ghadi14:04:47

clojure has a multimethod that allows you to participate in the printing system in a custom way for your deftype

ghadi14:04:53

you don't have to manually write to the Writer, you can make the map representation of your dreams and print that by delegation

ghadi14:04:39

(defmethod print-method Foo [my-foo ^java.io.Writer w]
  ;; delegate to calling print-method on your desired map
  (print-method (function-that-returns-the-desired-representation my-foo) w))

ghadi14:04:01

s/Foo/TightlyPackedTrie

Eric Ihli14:04:25

Bingo. Thanks! That's exactly what I was looking for.

(defmethod print-method TightlyPackedTrie [trie ^java.io.Writer w]
  (print-method (into {} trie) w))

🎉 6
alexmiller14:04:00

often you will also want to override print-dup in a similar way

Eric Ihli14:04:19

Is print-method/print-dup like Pythons __str__ vs __repr__ , the former being the human-readable repl format and latter is the string-serialized representation to recreate the object?

alexmiller14:04:00

I don't know python enough to answer that part, but sounds like the same idea

👍 3
alexmiller14:04:14

in clojure print vs pr

Andrei a. a.17:04:28

Hi, everyone. I am a clojure newbie and am super excited about learning and playing with this beautiful language! I am working my way through a roman numerals clojure exercise. I have

(def roman-numerals {"M" 1000 "D" 500 "C" 100 "L" 50 "X" 10 "V" 5 "I" 1})
and I'd like to convert let's say "XVI" to numbers - as a start. But
(map #(println %)  (sequence "XIV")) 
prints
X
(nilI
 nilV
 nil)

How can I get only the character and not the nil value so that something like this
(map #(get roman-numerals %)  (sequence "XIV")) 
produces numbers and not 
(nil nil nil)

?

noisesmith17:04:55

@aaandre what you see in the repl is the mixing of the side effect of printing with the value returned by printing

noisesmith17:04:32

further, map always calls seq on its input for you and #(println %) can be replaced by println

noisesmith17:04:55

user=> (run! println "XIV")
X
I
V
nil

Andrei a. a.17:04:56

How would I get (map #(get roman-numerals %) (sequence "XIV")) to produce the values of the hash correspoonding to the keys?

noisesmith17:04:03

run! is like map but for side effects

noisesmith17:04:24

if you want to process and not print, use the hash as a function

Andrei a. a.17:04:56

And, how would I convert (\X \I \V) to ["X" "I" "V"] ?

noisesmith17:04:57

user=> (map {\X 10 \I 1 \V 5} "XIV")
(10 1 5)

noisesmith17:04:08

you don't need to? or you can map str

noisesmith17:04:46

user=> (->> "XIV"
           (map str)
           (map {"X" 10 "I" 1 "V" 5}))
(10 1 5)

Andrei a. a.17:04:52

If (map {\X 10 \I 1 \V 5} "XIV") works, why doesn't (map roman-numerals "XIV") ?

noisesmith17:04:00

or you can be a functional programming nerd:

user=> (map (comp {"X" 10 "I" 1 "V" 5} str) "XIV")
(10 1 5)

sb17:04:03

#(roman-numerals (str %)) maybe in this way easier to understand (str chararcter type)

noisesmith17:04:29

@aaandre seq of a string provides characters, not strings

noisesmith17:04:58

@sb oh yeah that's how a normal person would write that huh :D

😄 3
Andrei a. a.17:04:15

So roman-numerals has strings for keys and seq produces characters, and characters are not strings?

Andrei a. a.17:04:32

(I am coming from ruby 🙂

noisesmith17:04:08

@aaandre my first approach would be to just use characters as keys since the input is characters

noisesmith17:04:35

the string as key is just a complexity introduced by your intuition about ruby IMHO

Andrei a. a.17:04:19

Got it. Changed roman-numerals to (def roman-numerals {\M 1000 \D 500 \C 100 \L 50 \X 10 \V 5 \I 1}) and now (map roman-numerals "XIV") works as intended

🍻 5
noisesmith17:04:28

but that's just a style thing, converting the characters to strings also solves the problem

Andrei a. a.17:04:28

You mean converting each character from the provided string to a string? how would I do that?

noisesmith17:04:42

I guess the fun part is going to be the state machine for the prefixes hu

Andrei a. a.17:04:00

Yes, one step at a time 🙂

Andrei a. a.17:04:22

You'll be hearing from me again!

Andrei a. a.17:04:51

Loving clojure so much and how it makes me think about problems. It. just. flows.

noisesmith17:04:59

@aaandre for a standalone seq to seq: #(map str %) but it's usually more helpful to use the function rather than the mapping as the building block

Andrei a. a.17:04:45

and how would I convert character to symbol - if I am to have a proper hash keys?

noisesmith17:04:55

so that's why (map roman-numerals input) can become (map (comp roman-numerals str) input) or (map #(roman-numerals (str %)) input)

👍 3
noisesmith17:04:34

@aaandre this is a thing that's different in clojure - we call :foo a keyword, not a symbol, and 'foo is a symbol

noisesmith17:04:51

a "proper" map can have keys that are any immutable type

noisesmith17:04:06

often a character or a string as a key is better than a keyword

Andrei a. a.17:04:15

got it thank you

Andrei a. a.17:04:30

(map keyword (map str "XIV"))

💯 3
noisesmith17:04:38

in fact adding code complexity by converting to keywords is a big pet peeve of mine (but once again that's a style thing and reasonable programmers can disagree)

em17:04:18

@aaandre Yeah it's sweet, I remember doing a integer -> roman numeral parser and the solution ended up representing the logic of the conversion, and not a hard coded table of all the symbols (which is what you'll basically strictly find with a google search), which was neat

noisesmith17:04:40

I mean, what actually makes {:a 0} better than {\a 0} ? if your domain is characters the second one is more straightforward IMHO

Andrei a. a.17:04:42

so defining the hash with keys closest to the data at hand is good practice despite data at hand may not be keywords - got it'

noisesmith18:04:09

right - keywords IMHO are great for when you have them as repeated constants throughout your code, but I don't think that's the role of the hash here

Andrei a. a.18:04:24

in my case it's roman numeral -> integer and it'll need logic, too as roman numbers sometimes indicate addition and sometimes subtraction, depending on positioning.

noisesmith18:04:44

right - but the subtask here is token -> integer

noisesmith18:04:50

why put keyword in the middle?

em18:04:54

yeah a state machine would be one way to do that

sb18:04:47

or “simply” with one transducer, like a csv parser

Andrei a. a.18:04:55

thank you for the help! I will be back when I need more guidance. Haven't be so excited about a language since I encountered Ruby.

Andrei a. a.18:04:00

Ok, another question: (map roman-numerals "XIV") produces (10 15 1) but anything I try to do with this results in an error. I don't even know how to call this structure - integers as expression? - and not sure how to convert it into something workable? Confused by map not returning a vector for example. What is the proper way to use the result of a map? Should I convert it to a vector? If yes, how?

noisesmith18:04:01

a list, when presented to the compiler, becomes an invocation

noisesmith18:04:06

invoking a number is an error

noisesmith18:04:32

(that's a lazy-seq not a list, but () as input makes a list)

Andrei a. a.18:04:33

how do I work with invocations consisting of the result of a map?

noisesmith18:04:49

by calling another function that expects a sequential input

noisesmith18:04:59

the ->> macro can help a lot here

noisesmith18:04:41

(ins)user=> (->> "abc")
"abc"
(ins)user=> (->> "abc" (map {\a 0 \b 1 \c 2}))
(0 1 2)
(ins)user=> (->> "abc" (map {\a 0 \b 1 \c 2}) (filter even?))
(0 2)

Andrei a. a.18:04:43

Interesting, even (map (1 2 3)) errors out.

noisesmith18:04:02

right, (1 2 3) is not a valid input to the compiler

noisesmith18:04:23

as I stated above, the compiler treats (...) as asking for invocation

noisesmith18:04:40

it's a valid printed form for a result, of course

yuhan18:04:56

it's only a problem when you try to copy and paste the output of an evaluation

noisesmith18:04:59

you can use [1 2 3] which will have the same behavior under map / filter / etc.

Andrei a. a.18:04:26

(filter even? [1 2 3]) works but (filter even? (1 2 3)) produces an error

noisesmith18:04:29

@qythium good point, you can use *1 to get the return value of the last expression

🤯 3
noisesmith18:04:01

@aaandre right as I said (...) asks for invocation, invoking numbers is invalid

Andrei a. a.18:04:19

OK, so map returns an invocation with all of its elements - how do I work with this?

noisesmith18:04:29

no, it returns something sequential

noisesmith18:04:11

@aaandre in a repl you can use def to assign a name to a return value

noisesmith18:04:36

just typing in what the last form printed will not usually be the same as using the value that was printed from

yuhan18:04:51

yup, you can also quote lists to prevent them from being evaluated like '(1 2 3)

yuhan18:04:18

but this might cause more confusion in the short term

noisesmith18:04:38

right - but that's pretty much never useful, since you can use [1 2 3] or (list* [1 2 3]) if you really need list instead of vector

Andrei a. a.18:04:27

I see, "something sequential" meaning "you can push this into anything that accepts a sequence"

Andrei a. a.18:04:37

But... (map even? (1 2 3)) produces an error and (map even? '(1 2 3)) does not

noisesmith18:04:59

right, that's because the ' operator prevents all evaluation

Andrei a. a.18:04:04

but what I get out of (map roman-numerals "XIV") is (10 1 5) without '

noisesmith18:04:23

right, because ' is a reader facility, not a data type

noisesmith18:04:09

'(foo) expands in the reader to (quote foo) and quote is a special instruction to the compiler that says "my arg is to be used as is and not compiled"

Andrei a. a.18:04:14

how can I turn (1 2 3) into [1 2 3] ? ... and should I?

noisesmith18:04:35

you are still misunderstanding the relationship between what the repl prints and the data itself

yuhan18:04:46

what you get out of (map roman-numerals "XIV") is a lazy sequence whose printed representation happens to be (10 1 5)

noisesmith18:04:51

you can use [1 2 3] in a repl for experimenting

Andrei a. a.18:04:02

a-ha! so I can't copy-paste the repl data because it's "printed"

yuhan18:04:21

when you enter (10 1 5) into the repl, clojure interprets it as "call the function 10 with the arguments 1 and 5"

noisesmith18:04:22

right - the printed form is sometimes but not always valid for evaluation

Andrei a. a.18:04:13

thank you again 🙂

Franco Gasperino21:04:13

(10 1 5)
=> nil
; 
; Execution error (TypeError) at (<cljs repl>:1).

`(10 1 5)
=> (10 1 5)

Franco Gasperino21:04:49

if you use a single quote / tick mark before the list, it'll not be considered a function

Franco Gasperino21:04:40

or you can transform it as you mentioned:

(into [] `(10 1 5))
=> [10 1 5]

Franco Gasperino21:04:41

(reduce conj [] `(10 1 5))
=> [10 1 5]

Franco Gasperino21:04:51

several ways to accomplish it

noisesmith18:04:35

in your code structure you can use let to bind results of expressions to names (that you can then use in all following forms in the let block), or directly wrap the expression in its consumer (with helpers like -> / ->> to avoid deep () nesting)

walterl22:04:33

What's the Right Way to do (update m :key conj 123), ensuring that :key's value is a vector (rather than a list) even if it didn't exist before?

walterl22:04:30

I.e. if m is {}, I want the result to be {:key [123]} and not {:key '(123)}

hiredman22:04:42

fnil

🎉 17
walterl22:04:53

Wonderful, thanks! (update {} :key (fnil conj []) 123)

walterl22:04:25

fnil is one of those super useful core functions that I don't use enough for its to "stick"

lsenjov23:04:13

TIL fnil exists

🎉 11
noisesmith18:04:12

95% of my fnil usage is either (fnil conj []) or (fnil conj #{})

👍 3
2
sova-soars-the-sora05:04:12

please teach a novice like me the ways of fnil foo (fnil fu)