Fork me on GitHub
#beginners
<
2021-10-30
>
olaf03:10:41

What I'm getting wrong here?

(defn foo [& {:keys [bar]}]
    bar)

(defn faa [fname & args]
  (let [fun (ns-resolve *ns* (symbol fname))]
    ; (println args)
    (apply fun args)))

(defn fee [fname & args]
  (faa fname args))

(fee "foo") ; => No value supplied for key: null
(fee "foo" :bar 1) ; => No value supplied for key: (:bar 1)
I expected args to be passed down, but logging inside faa , if I don't use any arguments, args becomes (nil) instead of nil breaking the following destructuring

seancorfield05:10:43

Yes, when you use & args destructuring, you need to use apply to call down the chain. This is why it is often better to pass a hash map of options or a vector of arguments down the chain instead of using a variadic function -- the latter doesn't compose very well (although it's nice for human interaction).

seancorfield05:10:37

This is why Clojure 1.11 supports passing a hash map to functions declared to destructure key/value pairs -- so you get the composability without losing the human interaction.

🙌 1
olaf05:10:35

Didn't know that! Is the cleanest solution, thanks :thumbsup:

olaf03:10:18

Should I use apply again inside fee or there's a better solution?

sova-soars-the-sora04:10:57

a partial is not a transducer?

hiredman05:10:36

A transducer is any function from a step function (a function suitable for passing to reduce) to a step function

hiredman05:10:48

Regardless of how it is created

sheluchin11:10:26

Are there any charting libraries that are popular with clojurians? I'm making progress with D3 but the API seems too low-level and complex for my foreseeable needs. I'm thinking about trying https://recharts.org/ instead, but wanted to see if there's a popular option when writing CLJS.

schmee12:10:12

I’ve been looking to try https://github.com/metasoarous/oz and https://github.com/jsa-aerial/hanami, might be what you’re looking for

GGfpc16:10:36

I have used recharts and it works well with CLJS

Shmarobes14:10:28

Hi, I am new here. I have recently started working on a simple telegram bot for me and my friends. There is a library just for that called https://github.com/Otann/morse, so I decided to use it. The idea is to implement a game: a user asks to play with something like "/play", the bot answers with a random question and waits for the player to submit an answer with "/answer ...". Then it updates player stats accordingly. I am now thinking what would be the best way to implement this behavior. I'll outline some of this task's challenges here, hoping for an advice on how to approach them. But please feel free to say that my design is all wrong and this should be done some other way. This is my first clojure project, so I am hoping to learn from you more experienced clojurians. The first problem I'm facing has to do with concurrency. What if another user sends "/play" command while the first one is still thinking about an answer? The second user should still be able to play. My initial idea is to use core.async's channels for this: create a thread (or a go block, I don't really understand what would change) when "/play" handler is called, which waits for an input form a certain channel (should be unique for each user). When the user finally submits an answer, the "/answer" handler puts on this channel, so the initial thread can continue working, checking if the answer is correct, then updating the player's score (which I want to implement as an atom, but more on that later). The actual problem here is this: where do I store the channels? The command handlers have no direct way of communicating (or at least I don't see one), so should I use a global var to store each player's channel? Or is there a cleaner solution to this? The second problem is about storing game data: player scores and predefined list of questions. I won't be able to keep the bot running 24/7, so there needs to be some way to transfer data between sessions. Should I load it from files on startup and dump to files when exiting? If so, how do I achieve the latter? Is it okay to use an atom for player scores and a simple var for questions? Should I store them as global references or is there another way? Lastly, what file format would you use? I'm thinking clojure data structures, then read them with read-string. As you can see, this project is fairly simple, but I'm really struggling with design, as I have no experience creating concurrent programs. I hope other beginners can also benefit from answers to this post. If something isn't clear, feel free to ask.

emccue15:10:15

This is the sort of thing that agents are really good for

🙌 1
emccue15:10:03

I have an example game I made for a school project that uses agents for a similar purpose

emccue15:10:56

(defn create-socket-backed-player!
  "Creates a player backed by a socket. Will
  potentially create some threads"
  [^Socket socket-conn]
  (let [server-side-state (agent {:listeners []
                                  :identifier nil})
        player (reify
                 Player
                 (identifier [_]
                   (:identifier @server-side-state))
                 (assign-id! [_ player-id]
                   (send-off server-side-state
                             (fn [state]
                               (util/write-to-socket! socket-conn {:kind :assigned-id
                                                                   :player-id player-id})
                               (assoc state :identifier player-id))))
                 (register-action-listener! [_ cb]
                   (send server-side-state update :listeners conj cb))
                 (update-state! [_ new-state]
                   (util/write-to-socket! socket-conn {:kind :new-state
                                                       :new-state new-state})))]
    (async/thread
      (try
        (while true
          (let [reader (transit/reader (.getInputStream socket-conn) :json)
                msg (transit/read reader)]
            (log/debug "Received socket message:" msg)
            (doseq [listener (:listeners @server-side-state)]
              (listener msg))))
        (catch Exception e
          (log/error "Error communicating with client" e))))
    player))

emccue15:10:17

;; ----------------------------------------------------------------------------
(defn create-referee-agent
  "Creates a new referee"
  []
  (doto (agent {:game-state gamestate/starting-game
                :players {}
                :observers []}
               :error-mode :continue)
    (set-error-handler! (fn [& _] (log/error _)))))

;; ----------------------------------------------------------------------------
(defn ^{:private true}
  inform-everyone-of-state!
  "Informs all players of the state of the game"
  [referee]
  (doseq [[_ player] (:players referee)]
    (player/update-state! player (:game-state referee)))
  (doseq [observer (:observers referee)]
    (observer/update-state! observer (:game-state referee)))
  referee)

;; ----------------------------------------------------------------------------
(defn ^{:private true} next-player-id [referee]
  (case (count (get referee :players))
    0 "white"
    1 "black"
    2 "red"
    3 "green"
    4 "blue"
    (throw (IllegalStateException. "A referee only manages up to 5 players"))))

;; ----------------------------------------------------------------------------
(defn ^{:private true} make-move! [referee move]
  (inform-everyone-of-state!
    (f/if-let-ok?  [next-game-state (gamestate/make-move
                                      (:game-state referee)
                                      move)]
      (assoc referee :game-state next-game-state)
      (f/if-let-ok? [with-player-kicked (gamestate/make-move
                                          (:game-state referee)
                                          {:kind :kick-player-for-invalid-move
                                           :player-id (:player-id move)})]
                    (assoc referee :game-state with-player-kicked)
                    (do (log/error "Cannot kick the player who made a wrong move")
                        referee)))))

emccue15:10:35

but truth be told, its probably more complicated than you would need

emccue15:10:37

and for the saving of data - yep for a simple thing you can just dump your clojure data to a .edn file

Apple16:10:55

for the second one, how about just save your data to a sqlite db. for the first one the program should be able to distinguish one user from another. each user has corresponding state in your bot.

Shmarobes21:10:25

@UP82LQR9N Yes I've considered using a db, but for an application of such a small scale it's a bit overkill. But maybe you're right: dbs are unavoidable, so why not practice now? Are there any resources for learning how to interact with dbs in clojure? Any good libraries? Thanks in advance.

Shmarobes22:10:03

@U3JH98J4R Thank you for your example. It is indeed more complicated than I need as it seems to handle server-client communications. As I understand, you're using an agent (referee) to store game state and players (represented as records). Could you please explain the advantages of using agents instead of atoms here? I've read that the only difference between the two is that when an agent's state is updated, it happens on a separate thread. Could you also tell me what those instructions starting with ^ are? I've seen them being used to denote that a function parameter should be of certain type, but that ^{:private true} confused me. Is it some kind of metadata?

Apple22:10:30

libs: org.xerial/sqlite-jdbc conman/conman mount/mount mount is optional conman is also optional but it's nice to use.

(ns xyz.db
  (:require [mount.core :refer [defstate]]
            [conman.core :as conman]))

(def pool-spec
  {:jdbc-url "jdbc:sqlite:xyz.db"})

(defstate ^:dynamic *db*
  :start (conman/connect! pool-spec)
  :stop  (conman/disconnect! *db*))

(conman/bind-connection *db* "sql/queries.sql")
queries.sql
-- :name find-users :n
select * from users
to use all the above
(ns xyz.core
  (:require [xyz.db :as db])

(db/find-users)

🙌 1
Apple22:10:24

use repl to play with the code it should get you going in no time. then if you need to go deeper just google the name of libs

Shmarobes22:10:16

@UP82LQR9N Thanks a lot! I'll try to use them in this project.

emccue22:10:22

@U02KJRX7HJR can you ping me tommorow and I'll give this a closer look?

emccue22:10:44

The main advantage was that agents handle all their messages in order

emccue22:10:02

So you can send off side effects within your handling code

emccue22:10:30

Atoms can potentially retry so you can't safely do side effects without another strategy

Shmarobes22:10:51

@U3JH98J4R Oh, so agents use some kind of a queue for updating their state instead of retrying as I understand it?

Shmarobes22:10:22

Got that bit about side effects, makes sense now.

Shmarobes22:10:50

I'll ping you tomorrow, yeah. Thanks for helping.

Shmarobes17:10:02

So, I've come up with a solution. I have all the code for the game in a separate namespace from the bot code. In this namespace I define an agent to store the players' data:

(def players (agent {})) ;; Should actually be read from somewhere on startup
Then I create two functions to use with send on this agent:
(defn challenge-player!
  "If the player doesn't have a challenge, get a random one, send a message 
  to the chat and return an updated players map with the challenge assigned."
  [players challenges player-name token chat-id]
  (if-not (get-in players [player-name :current-challenge])
    (let [{:keys [question] :as challenge} (rand-nth challenges)
           message (get-question-message player-name question)]
       (t/send-text token chat-id message)
       (assoc-in players [player-name :current-challenge] challenge))
     players))
This function checks whether the player already has a challenge. If not, it randomly selects one (for now challenges are stored in a vector as hash maps), then assigns it to the player by setting the :current-challenge property. It then sends a message to the chat with morse.api/send-text. The fact that an agent is used guarantees that the message won't be sent if the player already has a challenge. So yeah, an atom wouldn't work here because of potential retries. The same logic applies to the second function:
(defn submit-answer!
  "If the player has a challenge, check whether their answer is correct, 
  send a message to the chat and return a players map with updated scores. 
  Also set this player's :current-challenge to nil. Intended to be used with 
  `send` on an agent to avoid retries."
  [players answer player-name token chat-id]
  (if-let [challenge (get-in players [player-name :current-challenge])]
    (let [correct-answer (:answer challenge)
          answer-correct? (= answer correct-answer)
          message (get-answer-message 
                     player-name correct-answer answer-correct?)]
      (t/send-text token chat-id message)
      (->
        players
        (assoc-in [player-name :current-challenge] nil)
        (update-in [player-name :games-played] (fnil inc 0))
        (update-in [player-name :games-won] (if answer-correct? 
                                              (fnil inc 0) 
                                              identity))))
    players))
This one sets the player's challenge to nil, informs them about whether their answer is correct, then updates the stats. If the player doesn't have a challenge, nothing happens.

Shmarobes17:10:13

@U3JH98J4R Your advice to use an agent has solved my problem! I'd like to hear what you think about my solution. I've actually written the whole thing, and it works. So, thank you.

roelof14:10:22

I have this code :

`(defn update-output [_]
  (cond
   ((= \C (get-input-target) (= \F (get-output-temp)))
      (do (set-output-temp (c->f (get-input-temp)))
        (gdom/setTextContent degree "F"))
   ((= \F (get-input-target)) (= \C (get-output-temp)))
     (do (set-output-temp (f->c (get-input-temp)))
       (gdom/setTextContent degree "C"))
    :else (do (set-output-temp (get-input-temp))
    (gdom/setTextContent degree (get-output-temp))))))
but as soon as I try to compile it . I see this compile error
cond requires an even number of forms
What do I have done wrong ?

dpsutton15:10:59

You’re writing a scheme type cons where check and consequence are in the same list

dpsutton15:10:18

(Cond condition (f)) vs what you’ve done: (cond ((condition) (f)))

dpsutton15:10:47

So because of this cond needs an even number of forms. Each test has an associated consequence.

roelof15:10:59

sorry, I think I misunderstood you

roelof15:10:16

This is even less compiling

`(defn update-output [_]
  (cond
   ((= \C (get-input-target) (= \F (get-output-temp))))
      (do (set-output-temp (c->f (get-input-temp)))
        (gdom/setTextContent degree "F"))
   ((= \F (get-input-target)) (= \C (get-output-temp))))
     (do (set-output-temp (f->c (get-input-temp)))
       (gdom/setTextContent degree "C"))
    :else (do (set-output-temp (get-input-temp))
    (gdom/setTextContent degree (get-output-temp))))

hiredman16:10:01

You have too many parens clojures cond uses fewer parents then common lisp or scheme, read the docs find some examples

hiredman16:10:35

Ah, misread, now your problem is you have a few forms like ((= ... ...) (= ... ...))

roelof16:10:25

yep, I want to be both true

hiredman16:10:35

Which means you are call the result of the = (a boolean) call as a function

roelof16:10:42

or do I then need a and ?

hiredman16:10:11

(a b) is a function call

roelof16:10:10

still a error somewhere

roelof16:10:33

(defn update-output [_]
  (cond
   (and(= \C (get-input-target) (= \F (get-output-temp)))
      (do (set-output-temp (c->f (get-input-temp)))
        (gdom/setTextContent degree "F")))
   (and(= \F (get-input-target)) (= \C (get-output-temp))
     (do (set-output-temp (f->c (get-input-temp)))
       (gdom/setTextContent degree "C")))
    :else (do (set-output-temp (get-input-temp))
    (gdom/setTextContent degree (get-output-temp)))))
the parenteses makes my crazy now

hiredman16:10:34

In general I'd say to look at the error message

hiredman17:10:27

It looks like you are writing clojurescript not clojure, which can make the errors tricky

hiredman17:10:22

Buy if you are not compiling in advanced mode, it should be possible to figure things out

hiredman17:10:00

And cljs tends to have some better warning messages from the compiler

roelof17:10:29

I see no error messages only the layout is not chancing 😞

hiredman17:10:29

Then there is some logic error, you best bet is sprinkling prn calls around to see where things are diverging from your expectations

roelof17:10:03

oke, thanks

Apple18:10:34

use a decent editor then you'll see the parentheses error

roelof18:10:49

Since when is VS Code not a decent editor ?? :')

Apple19:10:49

vscode is great!

roelof20:10:23

oke because you said a decent one would help me to see where I did wrong and this time it did not

roelof20:10:12

but to come back to the code I need a extra ) in the first condition so I need to be

((= \C (get-input-target)) (= \F (get-output-temp)))

roelof20:10:45

maybe I can better start over again

sova-soars-the-sora20:10:53

If you want two things true use (and thing1 thing2)

sova-soars-the-sora20:10:28

@roleof (and (= \C (get-input-target)) (= \F (get-output-temp)))

sova-soars-the-sora20:10:08

Sometimes it helps to put it on mulitple lines

(and
  (= \C (get-input-target))
  (= \F  (get-output-temp))
)
Now we can make sure that the parens are balanced easily.

sova-soars-the-sora20:10:26

you can play in the REPL and see lein repl > (and (= 4 (* 2 2)) (= 5 (+ 1 1 1 1 1)))

pithyless20:10:18

@U0EGWJE3E if you indent the code like this, you can see that both the first and second condition has incorrect parens:

(defn update-output [_]
    (cond
      (and (= \C (get-input-target)
              (= \F (get-output-temp)))
           (do (set-output-temp (c->f (get-input-temp)))
               (gdom/setTextContent degree "F")))
  
      (and (= \F (get-input-target))
           (= \C (get-output-temp))
           (do (set-output-temp (f->c (get-input-temp)))
               (gdom/setTextContent degree "C")))
  
      :else
      (do (set-output-temp (get-input-temp))
          (gdom/setTextContent degree (get-output-temp)))))

pithyless20:10:55

here's what the indentation looks like if we correct the parens:

(defn update-output [_]
    (cond
      (and (= \C (get-input-target))
           (= \F (get-output-temp)))
      (do (set-output-temp (c->f (get-input-temp)))
          (gdom/setTextContent degree "F"))
  
      (and (= \F (get-input-target))
           (= \C (get-output-temp)))
      (do (set-output-temp (f->c (get-input-temp)))
          (gdom/setTextContent degree "C"))
  
      :else
      (do (set-output-temp (get-input-temp))
          (gdom/setTextContent degree (get-output-temp)))))

roelof20:10:11

oke, can vs code help me with the intendation

roelof20:10:59

and I wonder if there is a better way to make this work. Later I have to add Kelvin to the mix and then it will be I think some 7 conditions

roelof20:10:09

so a ver very big method

roelof20:10:42

maybe make smaller methods and check here alone on the first one

roelof20:10:53

then I could do it wirh 4

roelof20:10:13

tomorrow I will try this code

roelof20:10:27

time to sleep here and thanks all for the help

pithyless22:10:07

think of ways you can generalize the problem; here's a small refactor of your existing code to give you some ideas:

(defn temp-converter
    [from to]
    (case [from to]
      [\C \F] [c->f "F"]
      [\F \C] [f->c "C"]))


  (defn update-output [_]
    (let [[converter suffix] (temp-converter (input-target) (output-target))
          new-temp           (converter (get-input-temp))]
      (set-output-temp new-temp)
      (gdom/setTextContent degree suffix)))

sova-soars-the-sora15:10:03

if you find yourself duplicating lots of similar lines, consider how you can pull a variable out and de-duplicate

sova-soars-the-sora16:10:45

@U0EGWJE3E

;;4 ways to solve your problem
;; there are certainly more.

;;nested if statements

(def starting-symbol \C)
(def desired-symbol \F)

;;using the pattern
;;     (if thing-to-evaluate
;;        when-if-is-true
;;        when-if-is-false)


(if (= starting-symbol \C)
  (if (= desired-symbol \F)
    (set-output-temp (c->f @input-temp))  ;;c->f
    ;; else start is C and output is K
    (set-output-temp (c->k @input-temp))) ;;c->k
  ;;else starting symbol is not C
  (if (= starting-symbol \F)
    (if (= desired-symbol \C)
      (set-output-temp (f->c @input-temp))  ;;f->c
      ;;else start is F and output is K
      (set-output-temp (f->k @input-temp))) ;;f->k
    ;;else starting symbol is not C and not F
    (if (= starting-symbol \K)
      (if (= desired-symbol \C)
        (set-output-temp (k->c @input-temp)) ;;k->c
        ;;else start is K and output is not C
        (set-output-temp (k->f @input-temp)))))) ;;k->f
;;using (cond
;;         statement1  what-to-do-1
;;         statement2  what-to-do-2
;;         statement3  what-to-do-3 )
;;
;; more at 



(def starting-symbol \C)
(def desired-symbol \F)


(cond 
  (and (= starting-symbol \C)
       (= desired-symbol  \F))    (set-output-temp (c->f @input-temp))

  (and (= starting-symbol \F)
       (= desired-symbol  \C))    (set-output-temp (f->c @input-temp))

  (and (= starting-symbol \C)
       (= desired-symbol  \K))    (set-output-temp (c->k @input-temp))

  (and (= starting-symbol \F)
       (= desired-symbol  \K))    (set-output-temp (f->k @input-temp))

  (and (= starting-symbol \K)
       (= desired-symbol  \C))    (set-output-temp (k->c @input-temp))

  (and (= starting-symbol \K)
       (= desired-symbol  \F))    (set-output-temp (k->f @input-temp)))
;;using case with a vector [starting-symbol desired-symbol]


(def starting-symbol \C)
(def desired-symbol \F)


(case [starting-symbol desired-symbol]
  [\C \F] (do (set-output-temp (c->f @input-temp)
              (gdom/setTextContent degree "F")))
  [\C \K] (do (set-output-temp (c->k @input-temp)
              (gdom/setTextContent degree "K")))
  [\F \C] (do (set-output-temp (f->c @input-temp)
              (gdom/setTextContent degree "C")))
  [\F \K] (do (set-output-temp (f->k @input-temp)
              (gdom/setTextContent degree "K")))
  [\K \F] (do (set-output-temp (k->f @input-temp)
              (gdom/setTextContent degree "F")))
  [\K \C] (do (set-output-temp (k->c @input-temp)
              (gdom/setTextContent degree "C"))))
;;; crazy magic

(defn c->f [temp]
  (float (+ (/ (* temp 9) 5) 32)))


(defn converter-method [input-symbol output-symbol]
  "This takes something like 'C' and 'F' and 
  returns the function called c->f in the namespace"
  (let [in (clojure.string/lower-case input-symbol)
        out (clojure.string/lower-case output-symbol)
        funkshan-name (str in "->" out)
        fk-invokation (ns-resolve *ns* (symbol funkshan-name))]
        fk-invokation))

;;example: define c->f above and then call 
;; (converter-method \C \F)
;; returns
;; #'user/c->f     (the method in the namespace that looks like in->out)



;;test
((converter-method "c" "f") -40)
;;result is -40.0
;; first it translates the above to (c->f ...) 
;; then it invokes that function in the namespace with 
;; the argument -40
;; and returns the result -40.0


;;finished update-output statement
  (defn update-output [_]
      (do (set-output-temp ((converter-method (get-input-target) (get-output-temp)) (get-input-temp)))
          (gdom/setTextContent degree (get-output-temp)))

roelof16:10:11

oke, thanks

roelof16:10:47

I like 1 , 2 and 3 but then there is a lot of conditions in a method

roelof16:10:02

and I miss when someone chooses F ->F for example

sova-soars-the-sora16:10:54

@U0EGWJE3E yeah 3 might be easiest to read and you can always add [\F \F] as a case that returns the number unchanged

sova-soars-the-sora16:10:11

hope that helps :]

sova-soars-the-sora16:10:34

seemed like a good opportunity to showcase some different approaches for you to study at your leisure. maybe copy those to a file and look over them from time to time

roelof16:10:49

it helps always to see new ways to solve things

💯 1