Fork me on GitHub
#beginners
<
2023-02-12
>
Fredrik Andersson10:02:53

I have worked for a while with shadow-cljs and I enjoy the watch workflow. Now I'm looking into Clojure, lacinia and pedestal. I wonder how I could setup the same workflow. Particularly when a file is changed I want a stop function followed by compiler and update and then start function again. Is this possible with clj and deps.edn?

practicalli-johnny11:02:48

Including the web application (typically the router function) as a reference means that the router function or the handler functions it calls can be evaluated to pick up changes without restarting the application server (e.g. jetty, httpkit, etc)

(app-server/run-server #'app {:port http-port})
Or the application server can be restarted ( using either an atom based reference, mount, integrant, etc.), typically providing start / stop / restart functions The browser showing a web page from the system would have to be manually refreshed, unless some library or function was added to automatically do this

practicalli-johnny11:02:11

To avoid restarting the repl when adding dependencies, add-libs can be used to 'hotload' one or more libraries https://practical.li/clojure/clojure-cli/projects/hotload-in-project/

Fredrik Andersson13:02:47

Sorry, I was a little to eager. At the moment I'm using a user-namespace where I start the server when I start testing. Then I change my code, stop it via the user namespace, refresh changed namespaces with Conjure and then start the server again. I wish there were a solution that would stop, refresh and start each time I save a file.

Fredrik Andersson13:02:18

I'm not sure if you suggest my current workflow in your first suggestion?

Fredrik Andersson13:02:49

The browser refresh isn't important since that is done via shadow-cljs

Fredrik Andersson13:02:21

I'll watch the video

practicalli-johnny14:02:20

If only working in the one namespace, then evaluating the whole namespace should update functions for routing and handlers in the REPL, but nothing outside those functions. (you dont actually need to save the file to evaluate, but its recommended to keep things in sync) To reload everything, then clojure.namespace/refresh is probably the simplest (although mount, integrant, component, donut could be added for this too, but for just a web server that is probably overkill)

practicalli-johnny14:02:44

There is a discussion about reloading with Conjure in this thread https://clojurians.slack.com/archives/CK143P6D7/p1669365292967419

👍 2
popeye14:02:07

Whey does below code giving 2 as output ? I expected value 4

(def the-world (ref "hello" :min-history 10))

(do
  (dosync (ref-set the-world "better"))
  @the-world)

(let [exclamator (fn [x] (str x "!"))]
  (dosync
   (alter the-world exclamator)
   (alter the-world exclamator)
   (alter the-world exclamator))
  @the-world)

(ref-history-count the-world)

feng15:02:46

never really use refs and dosync during my 7 years of programming using clojure! because most of time, you need a database to coordinate the data, other than using refs in memory! most of the condition, atom is enough to define variable, and probably many condition, your’d using core.async channel and go thread to sequencial the event other than treat it as transactional, that’s useless most of the time

popeye16:02:06

yeah I agree,

popeye14:02:40

or is this, consider one value in dosync ?

delaguardo15:02:35

> All changes made to Refs during a transaction (via ref-set, alter or commute) will appear to occur at a single point in the 'Ref world' timeline (its 'write point'). > https://clojure.org/reference/refs from here

👀 2
skylize17:02:03

Haven't had need to use refs myself. So @U04V4KLKC's response did not seem all that helpful to me... Until I looked up the docs for dosync, (which I also never needed to use), where I learned that dosync is what makes this a transaction. > (dosync & exprs) > Runs the exprs (in an implicit do) in a transaction that encompasses exprs and any nested calls. ... > https://clojuredocs.org/clojure.core/dosync So dosync is creating a transaction context. All the expressions run in that context are applied as a single all-or-nothing transaction. Then as @U04V4KLKC pointed out, the whole transaction is recorded as a single write point.

popeye16:02:35

Just trying out few things in clojure repl today , Why repl is not ending execution after running (execute-tasks)
(def task-queue (async/chan 4))

(defn send-task [ & tasks]
  (doseq [task tasks] 
         (println "Adding task to the queue "task)
         (async/>!! task-queue task )))

(defn execute-tasks []
  (loop [] 
    (when-some [task (async/<!! task-queue)]
      (println (eval task))
      (recur))))

(comment
  
  (send-task `(+ 1 2))
  (send-task `(+ 1 2))
  (send-task `(+ 1 2))
  (send-task `(+ 1 2))
  
  (execute-tasks)
  )

popeye16:02:06

probably it is going in infinite loop ?

dpsutton17:02:19

(when-some [task (async/<!! task-queue)] will block forever trying to take from the task queue. You need to close the task when it is done, or put a value on the task queue to indicate to stop taking more

popeye17:02:24

is there any way where I can check all task is in queue is removed > so that I can cam close the channel at that time

dpsutton17:02:54

you can close the channel when you know no more items will be put on the queue

dpsutton17:02:33

user=> (defn execute-tasks []
  (future
    (println "starting task running on background thread")
    (loop []
      (if-some [task (async/<!! task-queue)]
        (do
          (println (eval task))
          (recur))
        (println "stopping task running loop")))))

user=> (execute-tasks)
#object[clojure.core$future_call$reify__8544 0x59fa5a39 starting task running on background thread
{:status :pending, :val nil}]
user=> (send-task `(+ 1 2))
Adding task to the queue  (clojure.core/+ 1 2)
nil
user=> 3
(send-task `(+ 1 2))
Adding task to the queue  (clojure.core/+ 1 2)

user=> (async/close! task-queue)
nil
user=> stopping task running loop

Charlot19:02:18

Couldn't find a better channel for this, but I'm trying out Selmer and having issues with keywords. I have them namespaced, like so: ::foo.bar/baz and I'm trying to reach them in a template with {{foo..bar/baz}} and it's not finding them. I can see from the docs and confirm in experiments that the template can find :foo.bar/baz . I could write a converter function, but that's just avoiding the issue.

pppaul19:02:34

::foo.bar/baz this is a namespaced keyword, you should not be using these in templates. if you were to change the namespace name it will break your templates.

pppaul19:02:33

actually, the way you made the keyword isn't valid clojure

pppaul19:02:05

just FYI ::foo and :foo mean different things in clojure

pppaul19:02:33

for the selmer part, i use keywords like :foo.bar/baz without issue, almost all of the keywords i use in templates are namespaced, and many are namespaced with periods in the namespace part.

pppaul19:02:44

can you print out the map that you are giving the template please

pppaul19:02:00

(render "{{person.name}}" {:person {:name "John Doe"}}) can you give an example like how the selmer docs do.

Charlot19:02:49

(def example-character
  #:char{:name "Tuesday Slithen"
         :level 4
         :strength 2
         :constitution 3
         :dexterity 4})

Charlot19:02:09

With the requirement at the top of [unwarysage.grimm.domain.character :as char]

Charlot19:02:13

{{unwarysage..grimm..domain..character/constitution}}

                {{unwarysage..grimm..domain..character/dexterity}}
Is the template

pppaul19:02:07

the input data and the template lookup don't match at all

pppaul19:02:28

#:char{:name "Tuesday Slithen" is weird, it may just be doing too much magic, can you use a regular clojure object instead?

Charlot19:02:52

So char in clojure is aliased to unwarysage.grimm.domain.character, but the template wouldn't know about that, so I typed it out fully in the template

Charlot19:02:54

One moment

pppaul19:02:30

i haven't tried this many periods in a namespace before in selmer, likely this is a problem

pppaul19:02:32

selmers code isn't as general as you may believe, so try the same thing but make the namespace with only a single period

pppaul19:02:19

if that works, then you can do something like clojure.set/rename-keys as a pre-processing step, and have cleaner templates as well

Charlot19:02:01

(render-character-sheet {} {:char/constitution 9}) this works, 9 shows up in the template (render-character-sheet {} {::char/constituion 9}), this doesn't. As I understand it, :: means that it's a hard alias. You have to have it imported to use it.

Charlot19:02:34

Wait a minute, typo

Charlot19:02:13

okay, without the typo the second one works

pppaul19:02:31

:: is a ns alias, it's useful for avoiding namespace collisions in keywords, you prob don't want to use it

Charlot19:02:35

So the reader macro construction has flaws.

Charlot19:02:29

I'm hoping to have a consistent set of specs for the system, hence the precisely namespaced keys.

pppaul19:02:47

you can use the ns keyword in clojure code, but translate them for the templates

pppaul19:02:28

you can have meaningful namespaced keywords that don't use the :: style

pppaul19:02:20

:: is handy when you really don't want other parts of your code using/changing data

Charlot19:02:37

Ah, I think I found my original issue. #::{} produces a different map compared to #:{}.

Charlot19:02:30

So, standardize on something like :grimm.character/constitution is your advice?

pppaul19:02:13

https://clojuredocs.org/clojure.set/rename-keys , https://clojuredocs.org/clojure.walk/prewalk-replace these 2 functions you can use to change your keywords for use in your template. it'll make your template much easier to read/write, and you have full control over the translations

Charlot19:02:16

(You should not be able to trigger emojis inside code blocks)

Charlot19:02:42

Also, nice Blastwave art

pppaul19:02:00

i don't really want to tell you how to write your code, cus maybe you have a good reason for using ns style keywords. you have 2 options, one is to change your keywords in your code, and one is to change your keywords in your template. i think you should try both and settle on what you feel is better

Charlot19:02:33

Fair, twas why I called it advice. I'll see what works.

Charlot19:02:40

Regardless, thank you

pppaul19:02:19

no problem. if you have any other issues with selmer i can prob help. i use it a lot and have contributed to it a few times.

pppaul19:02:56

i'm not really sure what you are doing with selmer (if you are just playing around, or your app is serious), but i would recommend trying to write your code so that it doesn't depended too heavily on selmer for rendering templates. having your render function dynamically dispatch to selmer or some other template lib (rum/hiccup) can be very useful

Charlot20:02:48

Yeah. Was planning on making a generic "render" function accessible in the system map. I'd considered hiccup, wrapped in a cache if necessary.

pppaul21:02:52

i have found it useful to use a multimethod for rendering, i actually do recursive rendering in parts of my app where i can mix selmer and other templates inside each other. (render {:template/type :selmer :template/file "file.html" :template/data {}}) something like that, though my data can contain other template defs, but i recommend you don't do something like that at first cus it's a little tricky.

Hans Lux19:02:59

Confusion about namespaced keywords and spec. I heard it's good to use namespaded keywords in my maps. Say I have a person model with fname and lname. Should the model be

{
::fname fname
::lname lname
}
or rather
{
:person/fname fname
:person/lname lname
}
And how do I write specs for that? For example: this implementation:
(def non-empty-string? #(not (str/blank? %)))

(s/def :person/fname
  non-empty-string?)

(s/def :person/lname
  non-empty-string?)

(s/def :person/person
  (s/keys :req-un [:person/fname
                   :person/lname]))

(defn make-person [fname lname]
  (s/assert :person/person
            {:person/fname fname
             :person/lname lname}))
passes this test
(deftest make-person-test
  (testing "creating a person"
    (are [fname lname person] (= (make-person fname lname) person)
                              "Fname" "Lname" {:person/fname "Fname" :person/lname "Lname"}
                              "Fname2" "Lname2" {:person/fname "Fname2" :person/lname "Lname2"}
                              )))
However, when I try to create a 'person' in the repl , I get an error:
*ns*
=> #object[clojure.lang.Namespace 0x768bdf96 "user"]
(people-i-know.model.person/make-person "Fname" "Lname")
Execution error - invalid arguments to people-i-know.model.person/make-person at (person.clj:17).
#:person{:fname "Fname", :lname "Lname"} - failed: (contains? % :fname)
#:person{:fname "Fname", :lname "Lname"} - failed: (contains? % :lname)
Obviously there is something about namespaces and specs I haven't understood.

hiredman19:02:33

The un in req-un means unqualified (no namespace) so your person spec is for a map with the keys :fname and :blame, which is why the spec fails, and why it reports that your map doesn't contain those keys

phronmophobic19:02:04

I'm not sure about the test part, but your spec is failing because the keys are specified with :req-un and make-person is using qualified (ie. namespaced) keywords

Hans Lux19:02:32

you are both right! 🙂 I somehow had req-un(ion) in my mind and thought it meant to apply all specs for the keys. Thank you very much for your quick and correct answer. It is working now.

phronmophobic19:02:59

Also, I would probably recommend making an explicit spec for the non-empty string:

(s/def ::non-empty-string?
  (s/and
   string?
   #(not (str/blank? %))))

(s/def :person/fname ::non-empty-string?)

phronmophobic19:02:55

this should make it possible to generate persons.

Hans Lux20:02:49

would you do that to make it more clear to the eye or also for technical reasons?

phronmophobic20:02:42

I think either could be fine depending on the use case and personal preference.

phronmophobic20:02:33

It's helpful if you (or someone who uses your specs) wants to do generative testing.

phronmophobic20:02:37

or generate examples

phronmophobic20:02:06

I think it might also slightly improve the output when explaining why a particular value doesn't conform.

Hans Lux20:02:59

thank you for that hint. I'm planning to look into generative testing, but it seems that I have to do a bit more reading on specs before.

👍 2