How do you interact with clojure libs to learn how they work?

How do you “step” into a library such that you can treat it like it’s part of your code base and do things like make changes and reload the code?

For example, lets say i have a project that uses deps.edn with a dep on ring/ring-json.

Then in emacs, i want to read the code for the fn wrap-json-body to get a better sense of what it does. I can easily get to the wrap-json-name space in the lirbary, but the file is “read only”. why? Well if i look at where the file is it’s in my .m2 directory.

Maybe that’s not an issue, though, maybe I just mark it as NOT read-only? It seems a tad risky, though to edit files in m2? well, if I give that try and add a line of code (maybe to capture some state) then when I save, it asks me to create a directory. I think what’s happening is the original read-only file was inside a jar, and this “new” file is a directory created in the m2 directory.

That doesn’t feel good, but maybe it’s ok?

regardless, now if I re-eval my code, I can capture the state in the library. Which is my desired goal.

However, this feels odd for the reasons I listed above.

The alternatives I can think of are to use a debugger like flowstorm or I could git clone the library locally and then use deps to point at the local/root. The second seems ideal, except for many libraries that aren’t deps projects, so I’m not sure I can use local/root.

I’m curious what you do to get feedback on libraries and code external to your project!

2 Likes

For isolated experiments or in some niche production cases, I create a “patches” namespace, e.g.

(ns blah.patches
   (:require [blah.core])
(in-ns 'blah.core)
;;do stuff here, maybe define new stuff, maybe monkey patch if it's needed in the moment...
(in-ns 'blah.patches)

Then I can get a runtime hook into this via whatever I’m working through (either using the require functionality if I’m in my own project, or in more constrained environs, just load-file).

(ns mystuff.core 
   (:require [blah.patches]
             [blah.core :as b]))

The patch namespace should load the lib first and inject its junk, which is then exposed via the b alias here. So my runtime changes are present, and available via :reload of the patches ns. If these are things that end up being useful upstream, the changes are isolated to the patches ns and can be pretty trivially isolated and merged. This often happens (for me) when I have to make some change on an isolated system, but the only useful information for illuminating the problem is also there (local)…so I need to get a fix in place and have something working before I mirror the minimal changes necessary in a more permissive environment upstream.

In the limit, depending on how “read only” the lib is, I may clone the repo and add it to source paths to hack on it at runtime. If things go far enough and I don’t expect changes to go upstream (or if my changes are niche), I can deploy my own artifact on clojars or with a git repo via the emergent git deps stuff and just depend on that if viable.

These are a couple of ways that have enabled a degree of interactive exploration and modification/extension for me over the years (primarily working within constrained / airgapped environs).

OTOH if I can just clone the repo and hack on it locally (using the source-paths route), even better for exploration and dynamic modification (and submitting contributions upstream if you come up with any…).

3 Likes

With leinginen, I would often use checkouts (ie. create a checkouts folder in your project, clone the git repo of library-in-question within, and it will automatically be used when you restart the repl).

With deps, you can use “local deps”: ie. some/lib {:local/root "/path/to/src/"}

You can redefine the function at the REPL, if you need a temporary change. If you want a permanent change, you need to pulldown the dependency, like git checkout, and edit that, then just point your deps.edn to the local source for it.

But if it’s just for debugging, I believe you can use the cider debugger on it, just eval as debug.

I regularly do this kind of things with Emacs + Cider and it’s been working very well for me.
For typical reading and debugging, read-only mode is fine - I can trace or debug the function without issues (step through it and see the values, etc.)
If I need to modify the code I disable the read-only mode and do that. I don’t need to save those changes because they are temporary anyway.

I also often use cider-open-classpath-entry to jump into a JAR and examine its contents.
There I can see all the namespaces/files in given library (often there’s even a README file) and just press ENTER to jump to a selected file.
This is an example of how the view looks like for the clj-pdf library:

6 Likes