How do you find all the free variables in a Clojure expression?
By free variables, I mean all the symbols that are not defined in the local environment (including all local environments defined inside the expression), not defined in the global environment (all symbols in the current namespace, including all those imported from other namespaces), and not a Clojure primitive.
For example, there is one free variable in this expression:
(fn [ub]
(* (rand-int ub) scaling-factor))
and that is scaling-factor
. fn
, *
, and rand-int
are all defined in the global environment. ub
is defined in the scope that it occurs within, so it's a bound variable, too (i.e. not free).
I suppose I could write this myself—it doesn't look too hard—but I'm hoping that there's some standard way to do it that I should use, or a standard way to access the Clojure compiler to do this (since the Clojure compiler surely has to do this, too). One potential pitfall for a naïve implementation is that all macros within the expression must be fully expanded, since macros can introduce new free variables.
In computer programming, the term free variable refers to variables used in a function that are neither local variables nor parameters of that function.
Advertisements. In Clojure, variables are defined by the 'def' keyword. It's a bit different wherein the concept of variables has more to do with binding. In Clojure, a value is bound to a variable.
Basically a free variable is a variable used in a lambda that is not one of the lambda's arguments (or a let variable). It comes from outside the context of the lambda. Eta reduction means we can change: (\x -> g x) to (g) But only if x is not free (i.e. it is not used or is an argument) in g .
A free variable is simply a variable which is not declared inside a given function, but is used inside it.
You can analyze the form with tools.analyzer.jvm
passing the specific callback to handle symbols that cannot be resolved.
Something like this:
(require '[clojure.tools.analyzer.jvm :as ana.jvm])
(def free-variables (atom #{}))
(defn save-and-replace-with-nil [_ s _]
(swap! free-variables conj s)
;; replacing unresolved symbol with `nil`
;; in order to keep AST valid
{:op :const
:env {}
:type :nil
:literal? true
:val nil
:form nil
:top-level true
:o-tag nil
:tag nil})
(ana.jvm/analyze
'(fn [ub]
(* (rand-int ub) scaling-factor))
(ana.jvm/empty-env)
{:passes-opts
(assoc ana.jvm/default-passes-opts
:validate/unresolvable-symbol-handler save-and-replace-with-nil)})
(println @free-variables) ;; => #{scaling-factor}
It will also handle macroexpansion correctly:
(defmacro produce-free-var []
`(do unresolved))
(ana.jvm/analyze
'(let [x :foo] (produce-free-var))
(ana.jvm/empty-env)
{:passes-opts
(assoc ana.jvm/default-passes-opts
:validate/unresolvable-symbol-handler save-and-replace-with-nil)})
(println @free-variables) ;; => #{scaling-factor unresolved}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With