Fork me on GitHub
#clojure
<
2022-02-12
>
neilyio04:02:52

I’m having a hard time understanding the relationship between a REPL started with clojure.core.server and the REPL that I see in my terminal with the clj command. Here’s a minimal reproduction of what I’m working with:

(ns user
  (:require
   [clojure.core.server]
   [integrant.core :as ig]
   [integrant.repl :refer [go reset reset-all]]))

(defmethod ig/init-key ::clj-repl [key opts]
  (clojure.core.server/start-server (assoc opts :name (name key))))

(defmethod ig/halt-key! ::clj-repl [key _]
  (clojure.core.server/stop-server (name key)))

(integrant.repl/set-prep!
 (constantly
  {::clj-repl
   {:accept        'clojure.core.server/io-prepl
    :address       "127.0.0.1"
    :port          6777
    :server-daemon false}}))
I start up Clojure with clj, and I’m prompted with a REPL. I initialize my Integrant system.
Clojure 1.10.3
user=> (go)
init clj-repl
:initiated
user=> 
In Emacs, I connect with inf-clojure to localhost:6777. Evaluation works well. Through inf-clojure, I evaluate:
(def foo "bar")
… resulting in the expected prepl response:
{:tag :ret, :val "#'user/foo", :ns "user", :ms 0, :form "(def foo \"bar\")"}
And intuitively, I can now go back to my Terminal REPL (started with clj) and evaluate foo:
user=> foo
"bar"
user=> 
Seems like the io-prepl and clj are sharing the user namespace. Makes sense, and the behavior that I was hoping for! However, when I reload with Integrant:
user=> (reset-all)
:reloading (system.main user)
:resumed
user=> 
…I evaluate foo again. It errors. This is also what I anticipated from the use of reset-all.
user=> foo
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: foo in this context
user=> 
However, back in Emacs + inf-clojure, I can continue to evaluate foo with my io-prepl connection…
(do foo)
…and it still evaluates to "bar". No error.
{:tag :ret, :val "\"bar\"", :ns "user", :ms 0, :form "(do foo)"}
The io-prepl the clj terminal REPL are both in the user namespace… but they’re clearly out of sync. This problem continues with every use of reset-all. It seems that io-prepl is somehow keeping its own state, which isn’t getting reset with the use of clojure.core.server/stop-server. What confuses me most is that it was initially in sync with the clj terminal’s REPL state. After reset-all, they’re out of sync, and each one seems to keep a state of its own. Has anyone seen this before?

hiredman05:02:07

That isn't correct

hiredman05:02:23

I've not used reset all or whatever

hiredman05:02:57

But names, vars, in namespaces are global, in a clojure runtime, and any repls will see the same state

hiredman05:02:18

If you think you are seeing something different, you are mistaken in some way

hiredman05:02:57

My guess as to how you are mistaken is your repls are not connected to the same clojure process

hiredman05:02:35

Oh, that is definitely the case, if you started a repl via clj

neilyio20:02:26

That’s the part I don’t really understand. It’s just one Clojure process, started with clj. And the REPLs are in sync until I call reset-all

hiredman20:02:02

I think similar kinds of things are possible, but exactly what you described is not, so are you describing exactly what you see, or summarizing/editorializing something more complicated?

hiredman20:02:19

E.g. if you hang on to a var object it doesn't magically get replaced with the new var when you reset (which just uninterns all the vars so you get new var objects)

hiredman20:02:36

In theory if the reset code maybe un-interned the namespace object itself, maybe you could get something like you've described, but the code reset all calls doesn't do that (ultimately calls this stuff https://github.com/clojure/tools.namespace/blob/master/src/main/clojure/clojure/tools/namespace/repl.clj#L67) and i just think on theory you could, I think it is likely, in the scenario you described, both repls still wouldn't behave like that

seancorfield05:02:35

@neil.hansen.31 I think all the reset/reload/refresh workflows are extremely problematic and generally recommend people avoid them. They have weird corner cases and often very counter-intuitive behavior -- and they are simply not necessary if you develop a good REPL-friendly workflow. As to the specific problems you are running into, I'd suggest asking in #integrant because you're doing some stuff that is very specific to that project.

☝️ 1
neilyio20:02:41

@U04V70XH6 Thanks for the perspective. I actually thought the reset/reload style was the standard REPL workflow. Can you suggest any resources about the style you suggest?

mjw21:02:44

@neil.hansen.31 Sean's fully capable of answering himself, but… here's a great talk he gave on this very topic 😅 https://m.youtube.com/watch?v=gIoadGfm5T8

seancorfield21:02:16

Also, Stu's excellent talk from a few years prior -- here's a link to the transcript if you prefer to read (as I do), which in turn links to the video on Vimeo: https://github.com/matthiasn/talk-transcripts/blob/master/Halloway_Stuart/REPLDrivenDevelopment.md

seancorfield21:02:38

If you're willing to spend money on a course to teach you about this sort of workflow, consider Eric Normand's REPL-Driven Development course: https://purelyfunctional.tv/courses/repl-driven-development-in-clojure/

seancorfield21:02:08

(I have taken that course and highly recommend it to everyone!)

neilyio21:02:59

@U04V70XH6 @UGTAV6LR2 These are perfect resources, thanks for your help.

apbleonard07:02:59

How often do you use https://clojuredocs.org/clojure.core/remove-ns ? Would like to recommend it to those who get themselves into repl difficulties but it looks like it leaves corner cases too.

seancorfield16:02:29

@U3TSNPRT9 I don't use it at all because it can break code that depends on certain things in the ns being removed. I occasionally use a snippet of code that removes refers, aliases, and interns from a ns: https://github.com/seancorfield/vscode-clover-setup/blob/develop/config.cljs#L83-L89

❤️ 1
seancorfield16:02:36

That cleans out the "inside" of an ns so you can do "load file" and get a pristine version without breaking any dependencies (because the ns object itself does not go away).

seancorfield16:02:20

Note that even (require ... :reload) can break dependencies on an ns sometimes (you'll note I have code in that file to (require ... :reload-all) but I almost never use that because it tends to break dependencies) -- I keep meaning to remove it!

apbleonard02:02:41

@U04V70XH6 Thanks - feel like there should be a "clean namespace" button/shortcut/menu item running this snippet in IDE integrated REPLs (cc @U0567Q30W 🙂). It would help those newbie co-workers who restart the REPL constantly and find it "unreliable" because redefining vars and aliases everywhere is more side-effectful than they realise.

hiredman06:02:15

I suspect some kind of confusion and just repls hooked to different processes instead of the same, which reloading won't effect one way or another