Fork me on GitHub
#beginners
<
2022-10-01
>
ig0revich17:10:39

Hello. I have fork of some repo and “lein run” works well but “lein uberjar” fails. Here is stack trace https://pastebin.com/FnLGLsaZ

ig0revich17:10:46

What it could be?

dpsutton18:10:33

You shouldn’t ping people individually until they voluntarily engage

1
dpsutton18:10:52

Caused by: java.lang.NullPointerException: Cannot invoke "clojure.lang.Namespace.importClass(java.lang.Class)" because the return value of "clojure.lang.Var.deref()" is null
	at mockmechanics.synthesizer__init.load(Unknown Source)

dpsutton18:10:10

it’s trying to import and failing

ig0revich18:10:02

But how to fix it 🙂 I’m sure that it works with “lein run”

Chase18:10:44

Putting (ns mockmechanics.synthesizer) at the top of the file seems to get you past that error. But then other errors are popping up

Chase19:10:38

The new errors are starting in property-mode.clj but I'm not sure what's going on there. The ns doesn't match the file name and there are a lot of unresolved symbols

ig0revich18:10:30

@seancorfield Maybe you can help 🙂

seancorfield19:10:55

First off, please use threads - post follow-ups to your original message in a thread, not in the main channel. Second, never @ people who have not already engaged in the current thread. It is extremely rude.

seancorfield19:10:58

In answer to your question, no, I can't help. I haven't used Leiningen for years. I switched to Boot in 2015 and then to the official CLI / deps.edn in 2018. Which is another reason you should not @ random people about stuff.

ig0revich20:10:03

I see. Sorry if it was rude. I understand your point of view.

Yu21:10:20

Hi, I just started learning clojure. Could you please advice ? Thank you

1
Bob B22:10:26

well, you can get the price and quantity from the map by calling the keywords on the map, e.g. (:price item) will return the price

Bob B22:10:13

if it helps, you could potentially make a function that takes just an item (one map from the vector) and computes the total price (price * qty) of that item, and then use that in the reducing function

Yu22:10:28

Hi, I couldn't find what 0 bill in the end of reduce does ? Thank you

Bob B22:10:05

as written, 0 is the initial value, which means it would be the first value of acc in the reducing function

Yu22:10:55

oh ok Thank you very much !!

Bob B22:10:21

In a call like (reduce + [1 2 3]), there's no initial value, and per the docs, that means the function gets called with the first two elements of the collection (in this case, (+ 1 2)). In the situation you're looking at here, the accumulator has a very different shape from the collection items. We want our reducing function to take consistent arguments, so we don't want to the first call to be on two maps, and then subsequent calls to be on a number and a map, so by supplying an initial value, the first reduction will be called with 0 (our initial value) and the first map in the vector

seancorfield22:10:29

@U044TFZTBRA I see Eugene gave you the complete answer to this exact same question just a few minutes ago on Zulip... /cc @U2FRKM4TW

Yu22:10:38

Yes he gave me the answer. I came up with question how this works. I am still reading the documentations to understand. I prefer using slack if he is here as well.

p-himik22:10:44

Of course I am. :)

🙌 1
seancorfield22:10:50

Slack is a much bigger community so you'll have more folks to help you here than on Zulip.

seancorfield22:10:10

Are you learning Clojure from a book or an online tutorial or...?

Yu22:10:58

I usually google stuff and watch youtube.

seancorfield23:10:18

I think you would make more progress if you had a bit of structure to follow -- the learn Clojure guide on http://clojure.org at a minimum, perhaps the free online Brave and True book (but ignore the Emacs section in it).

seancorfield23:10:47

Clojure is very different to other languages so trying to learn it without a guide is hard.

Yu23:10:06

One more thing I want to do is to Write a function add-to-bill that accepts two arguments.
The first one is the bill below. The second one is a vector of additional items.
The method returns a new bill with the additional items.
I feel like I need to use into ? 

(def bill [{:name "Green Tea Ice Cream" :price 2.5 :quantity 2}
           {:price 1.0 :name "Sticky Rice" :quantity 1}])

(defn bill-total [bill]
  (reduce (fn [total item] 
            (+ total (* (:price item) (:quantity item))))
          0 bill))
(bill-total bill)

(def items [{:price 2.1 :name "Mango" :quantity 1} 
            { :quantity 1:price 1.0 :name "StickyRice" }])
            
(defn  add-to-bill [bill items]
 )

(add-to-bill bill items)
;; returns
;; [{:name "Green Tea Ice Cream" :price 2.5 :quantity 2}
;;  {:price 1.0 :name "Sticky Rice" :quantity 2}
;;  {:price 2.1 :name "Mango" :quantity 1}]

seancorfield23:10:40

Yes, if you have a vector of items and a second vector of items, into will produce a single vector with all those items in it.

seancorfield23:10:53

user=> (into bill items)
[{:name "Green Tea Ice Cream", :price 2.5, :quantity 2} {:price 1.0, :name "Sticky Rice", :quantity 1} {:price 2.1, :name "Mango", :quantity 1} {:quantity 1, :price 1.0, :name "StickyRice"}]
user=>

seancorfield23:10:20

Note that data is immutable in Clojure so that will not change bill, it will produce a new value.

seancorfield23:10:48

Experimenting in a REPL is a great way to learn what all these functions do and you have access to their docs and their source directly in the REPL:

user=> (doc into)
-------------------------
clojure.core/into
([] [to] [to from] [to xform from])
  Returns a new coll consisting of to-coll with all of the items of
  from-coll conjoined. A transducer may be supplied.
nil
user=> (source into)
(defn into
  "Returns a new coll consisting of to-coll with all of the items of
  from-coll conjoined. A transducer may be supplied."
  {:added "1.0"
   :static true}
  ([] [])
  ([to] to)
  ([to from]
...

Yu23:10:51

ok that's now what I want, stickey rice should be added to bill then quantity = 2

seancorfield23:10:56

Ah, you want same-named items to be added together? That's going to be trickier. What we tend to do is to reorganize our data to provide the best structure for operations we want to perform on them. In this case, you really want your bill to be a hash map from the name of each item to the information about it (quantity, price) and then you add an item by either adding a new name -> data entry if it isn't already in the bill or by combining it with the existing data entry if it is present.

seancorfield23:10:10

For example:

user=> (def bill-by-name (into {} (map (juxt :name identity) bill)))
#'user/bill-by-name
user=> bill-by-name
{"Green Tea Ice Cream" {:name "Green Tea Ice Cream", :price 2.5, :quantity 2}, "Sticky Rice" {:price 1.0, :name "Sticky Rice", :quantity 1}}
user=>

Yu23:10:58

I guess conj could be useful ?

seancorfield23:10:36

user=> (defn add-item [bill item]
  (if-let [existing (get bill (:name item))]
    (assoc bill (:name item) (merge-with (fn [v1 v2] (if (number? v1) (+ v1 v2) v1)) existing item))
    (assoc bill (:name item) item)))
#'user/add-item
user=> (add-item bill-by-name {:price 2.1 :name "Mango" :quantity 1})
{"Green Tea Ice Cream" {:name "Green Tea Ice Cream", :price 2.5, :quantity 2}, "Sticky Rice" {:price 1.0, :name "Sticky Rice", :quantity 1}, "Mango" {:price 2.1, :name "Mango", :quantity 1}}
user=> (add-item *1 { :quantity 1 :price 1.0 :name "Sticky Rice" })
{"Green Tea Ice Cream" {:name "Green Tea Ice Cream", :price 2.5, :quantity 2}, "Sticky Rice" {:price 2.0, :name "Sticky Rice", :quantity 2}, "Mango" {:price 2.1, :name "Mango", :quantity 1}}
user=>

seancorfield23:10:04

user=> (defn add-items [bill items] (reduce add-item bill items))
#'user/add-items
user=> (add-items bill-by-name [{:price 2.1 :name "Mango" :quantity 1} { :quantity 1 :price 1.0 :name "Sticky Rice" }])
{"Green Tea Ice Cream" {:name "Green Tea Ice Cream", :price 2.5, :quantity 2}, "Sticky Rice" {:price 2.0, :name "Sticky Rice", :quantity 2}, "Mango" {:price 2.1, :name "Mango", :quantity 1}}
user=>

seancorfield23:10:52

The add-item function could be written several ways. It could be explicit about building a hash map with [:price (+ (:price existing) (:price item)) :name (:name existing) :quantity (+ (:quantity existing) (:quantity item))} for example.

seancorfield23:10:07

And if you want your original format of bill back from bill-by-name you can call vals:

user=> (vals *1)
({:name "Green Tea Ice Cream", :price 2.5, :quantity 2} {:price 2.0, :name "Sticky Rice", :quantity 2} {:price 2.1, :name "Mango", :quantity 1})
user=>

seancorfield23:10:58

Another approach, without changing the data structure format, would be to map over the original bill and if the name of the entry matches the name of the new item, produce an added-up item, else the original.

seancorfield23:10:27

Edit: this does not work because it does not add new items!

user=> (defn add-item [bill item]
  (map (fn [bill-entry]
         (if (= (:name bill-entry) (:name item))
           (merge-with (fn [v1 v2] (if (number? v1) (+ v1 v2) v1)) bill-entry item)
           bill-entry))
       bill))
#'user/add-item
user=> (add-items bill items)
({:name "Green Tea Ice Cream", :price 2.5, :quantity 2} {:price 2.0, :name "Sticky Rice", :quantity 2})
user=>

seancorfield23:10:59

(that uses bill as a vector of items and the same add-items I showed above)

Yu23:10:41

Hi, quick question, since I declarer adding items to be items. so in the function parameter, it should be items ?

Yu23:10:26

Also {:price 2.1 :name "Mango" :quantity 1} disappeared ?

seancorfield23:10:10

Oh, you're right. My map version doesn't work, sorry.

seancorfield23:10:50

The parameter names don't need to match the global var names. But items is a sensible name for the parameter.

seancorfield23:10:05

(I updated my posts above -- I think the name -> data entry format is the best approach here anyway)

Yu23:10:10

I like your map version. is it possible to add else part to add new one if there is not match ?

Yu23:10:56

Also I not sure which one you edited sorry.

seancorfield23:10:19

No, map always produces the same number of items as the input has. It can't add items.

seancorfield23:10:07

I played around a bit more and I think I like this version best of all so far:

user=> (defn add-item [bill {:keys [price quantity name] :as item}]
  (if (contains? bill name)
    (-> bill (update-in [name :price] + price) (update-in [name :quantity] + quantity))
    (assoc bill name item)))
#'user/add-item
user=> (defn add-items [bill items]
  (-> (reduce add-item
              (-> (group-by :name bill) (update-vals first))
              items)
      (vals)))
#'user/add-items
user=> (add-items bill items)
({:name "Green Tea Ice Cream", :price 2.5, :quantity 2} {:price 2.0, :name "Sticky Rice", :quantity 2} {:price 2.1, :name "Mango", :quantity 1})
user=>
This uses the original bill (vector of items) and converts it to the "by name" version in add-items in a way that I think is more obvious than I originally suggested. And the new add-item here is simpler -- using map destructuring to provide local bindings directly to the pieces inside the item.

seancorfield00:10:56

See above. add-items is the answer to Q2.

seancorfield00:10:40

add-items in my code is add-to-bill in the specific question.

Yu00:10:42

Thank you very much. I would spend whole weekend to understand what's going on here 😇

seancorfield00:10:58

It's easier to solve if you break it down into 1) write a function to add a single item (hash map) to a bill 2) write a function that adds multiple items to a bill (using the function from 1).

seancorfield00:10:36

My last version has a number of things in it that you'll need to understand/learn as you go through your journey: map destructuring (the :keys/`:as` stuff), threading -> (and its various siblings ->>, some->, cond->, etc), and update-in for updating items nested inside hash maps.

seancorfield00:10:50

There are usually many ways to solve a problem in Clojure because there are so many different ways you can transform data (and so many core functions!).

Yu00:10:51

I deleted (update-in [name :price] + price) part since price should be stays the same until I calculate the total bill.

seancorfield00:10:48

Ah, I missed that. Yes, of course, price per quantity, not total price.

🙌 1
seancorfield00:10:16

At least with that update-in format it's easy to remove just one of the sums.

seancorfield00:10:21

Now you don't need the (-> bill (update-in ..)) you can say (update-in bill [name :quantity] + quantity) as the whole arm of the if

seancorfield22:10:59

Here's a direct link to the OP Q that has since been defeated: https://clojurians-log.clojureverse.org/beginners/2022-10-01/1664661320.776819 -- unfortunately, it doesn't preserve code blocks. However, they'd asked the exact same Q on Zulip, so here that is: I am trying to write a function bill-total whose one argument is a vector of such maps and returns the total of the bill. This is what I have so far. Could you please advice ? Thank you

(defn bill-total []
  )

(def bill [{:name "Green Tea Ice Cream" :price 2.5 :quantity 2}
           {:price 1.0 :name "Sticky Rice" :quantity 1}])
(bill-total bill) // should returns 6.0

Naoki Kaneko23:10:08

I want to validate that Person has a field named age and the value of age must be positive integer. Is the use of namespace and :req-un appropriate? If this is not the general way of writing, and you have a better way, could you please let me know?

(defrecord Person [age])
(defrecord Age [age])

(s/def ::age pos-int?)
(s/def :age/age (s/keys :req-un [::age]))
(s/def ::person (s/keys :req [:age/age]))

(s/valid? :age/age (->Age 20))
(s/valid? :age/age (->Age -10))
(s/valid? ::person (->Person (->Age 10)))
(s/valid? ::person (->Person (->Age -10)))

seancorfield23:10:54

We generally would not use records here, just plain maps. Your person would be a hash map like {:age 42} And then your specs will be simpler:

(s/def ::age pos-int?)
(s/def ::person (s/keys :req-un [::age]))

seancorfield23:10:01

user=> (s/valid? ::person {:age 42})
true
user=> (s/valid? ::person {:name "Sean"})
false
user=> (s/explain ::person {:name "Sean"})
{:name "Sean"} - failed: (contains? % :age) spec: :user/person
nil
user=>

seancorfield23:10:45

Even with records, you only need Person:

user=> (defrecord Person [age])
user.Person
user=> (s/valid? ::person (->Person 42))
true
user=> (s/valid? ::person (map->Person {:name "Sean"}))
false
user=>

Naoki Kaneko23:10:07

Fair enough. Thank you for your answer ! I understand that in this case using records is not appropriate or too much

didibus01:10:32

You can look at an example of domain modeling using maps and spec here: https://github.com/didibus/clj-ddd-example/blob/main/src/clj_ddd_example/domain_model.clj

Naoki Kaneko09:10:46

Thanks a lot! This is a kind of stuff I have been looking for!! Actually I am reading the book, Domain Modeling Made Functional and trying to convert the code in F# into cloture

👍 1
didibus16:10:58

That's why I put that repo together, DDD in Clojure is very easy, straightforward, and doesn't really require much code or any amount of cruft. It really just lets you focus on the actual domain problem and logic, and really you barely need to do anything in terms of designing patterns. Like my repo show, simple make-entity fns for constructing valid domain entities/values/aggregates, with spec as the layer to define and validate domain invariants. And you're pretty much ready to go.

didibus16:10:12

Maybe I should get that book and re-create its example in Clojure, might do that one day, seem it would be a good example for people.

yes 2
thanks3 1