Skip to content

tommy-mor/spy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

spy

spy lets you capture local variables at runtime and access them directly in your REPL. Wrap a block with spy, and every subexpression becomes instantly evaluable.

"In hindsight, so much of what we hype up as 'exploratory programming' in the REPL is really just coping with the lack of useful type information."
this post

"It's much easier for me to generalize from the concrete than concretize from the general."
— a professor once told me


Example: Cat Facts

Here's a real-world example fetching a cat fact from an API:

(require '[clojure.data.json :as json])

(spy
  (let [data (slurp "https://catfact.ninja/fact")
        processed (json/read-str data :key-fn keyword)
        {:keys [fact]} processed]
    (str "your fact is: " fact)))

After evaluating this:

;; Instantly accessible in the REPL:
data       ;; => "{\"fact\": \"Cats have 9 lives\", \"length\": 15}"
processed  ;; => {:fact "Cats have 9 lives" :length 15}
fact       ;; => "Cats have 9 lives"

;; Run any subexpression in-place, ithout modifying it
(str "your fact is: " fact) ;; => "your fact is: Cats have 9 lives"

;; Experiment live:
(+ (:length processed) 10) ;; => 25

No print statements, no manual defs—just instant access to every step of the computation. You can write expressions on the data without rerunning the api call!


How It Works: Before and After

spy is a macro that transforms your code to define all local variables globally. Here's what happens:

Before (your code):

(let [x 10
      y 20
      z (+ x y)]
  (* x y z))

After (macro expansion):

(let [x 10
      y 20
      z (+ x y)]
  (def x x)
  (def y y)
  (def z z)
  (* x y z))

After evaluation, x, y, and z are available in your REPL as 10, 20, and 30. You can mess around with them instantly:

(* z 2) ;; => 60

Motivation

I've leaned on the "inline def" trick—(def x x)—for years to debug and develop interactively. (See great write-ups here and here.)

But it's tedious:

  • Manually writing (def arg1 arg1) for every variable is a chore.
  • You've got to clean them up before committing to avoid code smell.

spy automates this pattern, making it effortless and keeping your code clean. It's the inline def hack on autopilot.


Trade-Offs

Yes, spy clogs up the global namespace with defs—just like (def varname varname) does. This means that you can't ever have a local variable named count, because then it will overwrite the count var, and mess up other code that expects count to be a function. Using spy you have to be more careful about naming your local variables.


How to Use

  1. Add spy to your project by adding this to your deps.edn
{:deps {io.github.tommy-mor/spy {:git/sha "COMMIT-SHA-HERE" :deps/root "spy"}}}

or, copy the code directly into your user.clj, its only 30 lines.

  1. Require it:
(require '[tommy-mor.spy :refer [spy]])
  1. Wrap any block:
;; instrument expression/function
(spy
   (defn test-fn [a {:keys [b c]}]
     (+ a b c)))

;; initalize values
(test-fn 10 {:b 20 :c 30})
  1. Evaluate and explore:
a ;; => 10
b ;; => 20
c ;; => 30
(+ a b c) ;; => 60

Comparison to type systems.

In typed languages, you write an expression and get millisecond-level feedback like List<Integer>, offering instant but abstract type info. With spy, you eval once and get near-instant access to concrete values like [1, 2, 3, 4]—richer for exploration, though it needs that initial run. It's an improvement over types: instead of just knowing the shape, you see the real data and can evaluate subexpressions in context immediately. Types have other benefits that spy does not have.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published