Fork me on GitHub
Jose Varela04:04:13

Hello everyone! Been dipping my toes with Clojure these past few days and ended up in a small rabbit hole on how to make the REPL workflow smoother. Posting here to see if there’s a ‘canonical’ setup. I read Stuart Sierra’s post (dives deeper in and which was later reified (did I use that correctly? lol) in and In a gist, the workflow suggests: • Creating a dev profile (eg Leiningein) • Avoiding global vars/defs (except for system which you can interact with in the REPL) • Building your application as a system made of components (eg • Interact with the system using functions available in dev/user.clj (init, restart, go, stop) • Using tools.namespace’s refresh to make sure your REPL is up to date with your code as you make changes There’s a bit of overhead in setting this up so I wanted to post here to increase my confidence that I’m on the right track. Another concern is that most of this content is from 2013-14, although I found references in this from 2018. My question: Is this still a sane approach to keep a smooth REPL workflow? Or have these ideas further been built into libraries that I should look at to avoid having to tweak/debug/update my workflow later on? Right now I’ve been using Calva and Portal and it’s pretty cool to say the least. But once you get a taste of a REPL workflow, you start noticing everything that takes even a few seconds. What can I say, a few days into Clojure and I’m already spoiled by the interactivity 😂


It depends what kind of application you're working on. The reloaded workflow is good when say you have a webserver and a database, and maybe one or two remote clients, and they all need to be initialized in your REPL, and then you change something about them but they don't "reload" magically like a defn does, so you have to destroy and re-initialize them, or restart the REPL. That's when a reloaded workflow comes in.


At work, we rely on Component very heavily and I think my REPL workflow is extremely good -- but I do not think "reloaded" is needed: I do not use that machinery, although I do occasional stop one system and start another, while working on a monorepo with over a dozen web applications, in a single REPL. I never use tools.namespace and I think the "refresh" approach is very overrated (and can cause the unwary all sorts of problems).


For example, in the web app I have open right now in VS Code, I have this RCF at the bottom:

  (require '[com.stuartsierra.component :as component]
           '[worldsingles.application :as app])
  (def a "API test component" (component/start (app/system {:process-id :test})))
  (component/stop a)
  (require 'clojure.repl)
That lets me start and stop the app as needed when I'm working -- and I do all my development via evaluation from my editor into the running application, started from my REPL. I never type into my REPL.


It's long but here's a presentation I gave to London Clojurians about my REPL-based workflow:


There are some useful tips here (that whole REPL guide is worth reading but especially that page).

Jose Varela06:04:18

Thank you both for the guidance, and thanks @U04V70XH6 for the materials! Will go through those. Most likely I’ll stick with Component and let go of the tools.namespace/refresh approach for now.


My personal advice would be to avoid even Component until you understand the need for it, by literally going: "man, I wish there was something to manage..." and then you go and find Component.


Especially in my experience, using something like that immediately can be really tempting to not have any pure functions, and simply inject components everywhere. That said, if you're going to be using your side-effectful state (like you DB), directly in every leaf function of your business logic, you might as well inject it and wrap it in a Component to do so. But ideally you don't do that if you can.


"What can I say, a few days into Clojure and I’m already spoiled by the interactivity 😂 " I know my app is a success when the user requests improvements. 🙂


I feel kinda dumb here, but I can't seem to figure this out. I have setup a project using stuart sierras component and am trying to use tools.namepsace with it. However, when I use:

(repl/refresh :after 'user/go)
It throws this exception:
:reloading (central.config central.middleware central.server central.database central.system user)
:error-while-loading central.middleware
;; => #error {
 :cause "namespace 'central.config' not found"
 [{:type clojure.lang.Compiler$CompilerException
   :message "Syntax error compiling at (central/middleware.clj:1:1)."
   :data #:clojure.error{:phase :compile-syntax-check, :line 1, :column 1, :source "central/middleware.clj"}
   :at [clojure.core$throw_if invokeStatic "core.clj" 5856]}
  {:type java.lang.Exception
   :message "namespace 'central.config' not found"
   :at [clojure.core$apply invokeStatic "core.clj" 669]}]
 [[clojure.core$apply invokeStatic "core.clj" 669]
  [clojure.core$load_libs invokeStatic "core.clj" 5974]
  [clojure.core$load_libs doInvoke "core.clj" 5958]
  [clojure.lang.RestFn applyTo "" 137]
I definitely have a central.config namespace as the app starts runs and I can stop it. However, using repl/refresh seems to break everything Could anybody shed some light here?


Wooooooooooow - thank you so much!


Hi there. If I have a list like this '(1 2 foo) and foo is defined as (def foo 3). Is there any way to resolve foo to 3 in such list, i.e. something similar to ~? I'm using list as the moment but want to see if there's another way of achieving the same thing.

Martin Půda07:04:31

(def foo 3)

`(1 2 ~foo)
=> (1 2 3)


you can also just create a list normally

(def foo 3)
(list 1 2 foo) ;; (1 2 3)


list is what I'm using at the moment, but it seems like backquote is neater


Thanks for the help


I find list to be more readable than the ` + ~ syntax, but that's just me.

☝️ 5

it's far more normal to have [1 2 foo] - using lists is rarely neccessary


I'm using pathom so I need list instead.

Serafeim Papastefanos12:04:51

Can you explain me what this syntax does (def ^:private initial-user-data

Serafeim Papastefanos12:04:14

Mainly the ^:private part

Cora (she/her)12:04:51

it means {:private true}

Cora (she/her)12:04:10

and it adds that as metadata to the following thing, namely the function

Cora (she/her)12:04:37

scroll down to the "Metadata Reader Macros" section and hopefully that'll answer your questions 😊

🙌 1

Hi, I abstracted the data store/load methods I have into a protocol so perhaps I can replace the db I use now with something else. To use, I created an atom and assigned the db object to it. This leads to me having to recreate the db object everytime I change it. Is this a common issue and/or what's the way to handle this?


Can you Include an example?


;; bookkeeper.clj
(def store (atom nil))
(defn set-store! [s] (reset! store s))

(defprotocol Store
  "Protocol for storage of ledger and related entities"
  (save-ledger! [s ledger])
  (get-ledger-by-name [s name])
  (get-ledgers [s])
  (save-account! [s account ledger]))


(defn get-ledgers-info []
  (get-ledgers @store))
;; xtdbimpl.clj

(defn -get-ledgers [node]
  (q node '{:find [?name ?id]
            :where [[?id :ledger ledger] [?id :name ?name]]}))

(deftype xtdb-store [xtdb-node]
  (save-ledger! [s ledger] (-save-ledger! xtdb-node ledger))
  (get-ledger-by-name [s name] (-get-ledger-by-name xtdb-node name))
  (get-ledgers [s] (map
                    (fn [[n i]] {:name n :id i})
                    (-get-ledgers xtdb-node)))
  (save-account! [s account ledger] (-save-account! xtdb-node account ledger)))
;; core.clj

(ns dumrat.core
  (:require [dumrat.api :refer [start-server stop-server]]
            [dumrat.xtdbimpl :refer [->xtdb-store]]
            [dumrat.bookkeeper :refer [set-store!]]    
            [dumrat.xtdb-init :refer [start-xtdb! stop-xtdb!]])

(def s (atom nil))

(defn boot []
  (reset! s (start-xtdb!))
  (set-store! (->xtdb-store @s)))
;; api.clj

(ns dumrat.api
  (:require [ring.adapter.jetty :as jetty]
            [compojure.core :as comp]
            [dumrat.views :as v]
            [dumrat.bookkeeper :as bk]))

(comp/defroutes routes
  (comp/GET "/" req (v/main req))
  (comp/context "/ledger" []
    (comp/GET "/view-list.html" req
      (v/show-ledger-list (bk/get-ledgers-info)))
    (comp/GET "/show-ledger" req
              (v/show-ledger (get-in req [:params :id]))))
  (route/not-found {:status 404
                    :body "Not found."
                    :headers {"Content-Type" "text/plain"}}))


I have to restart repl everytime I change a method in xtdb-store in xtdbimpl.clj


I feel you've got too many mutable things, not sure if you realize, but in deftype:

(deftype Foo [bar])
bar is a variable contained inside the Foo instance now, it is not Foo itself


By changing a method, you mean on the deftype/protocol, or one of the implementing defns like -save ?


You cannot change methods on an existing instance of a type, you need to re-create the instance. So if you change the methods on deftype itself (not the implementing functions, the methods themselves), existing instances of the type won't see the update, so whatever was using those instances you need to create a new instance and pass the new instance to those places.


Most likely in your case you'd have to call: (set-store! (->xtdb-store @s)) again


Hi, What is clojure cli tools' equivalent of <localRepository> tag inn .m2/settings.xml.


Thanks a lot Alex