2

Comming from Common Lisp, I'm trying to use let to shadow the value of a global variable dynamically.

(setv glob 18)

(defn callee []
  (print glob))

(defn nonl [x]
  (callee)
  (let [glob x]
    (callee))
  (callee))

(nonl 39)

=>
18
18
18

Is there a way to make this work so that the second call to callee gives 39?

[EDIT]

Based on gilch's response i wrote the following draft using contextvars:

(import contextvars :as cv)

(defmacro defparam [symbol value]
  (let [command (. f"{symbol} = cv.ContextVar('{symbol}')")]
    `(do (exec ~command)
         (.set ~symbol ~value))))

(defmacro parameterize [symbol value #* body]
  `(. (cv.copy-context)
      (run (fn [] (do (.set ~symbol ~value)
                      ~@body)))))

(defn callee []
  (glob.get))

;;;;;;;;;

(defparam glob 18)

(callee)                  => 18
(parameterize glob 39
  (callee))               => 39
(callee)                  => 18

Thanks for the answers!

2 Answers2

2

There's no built-in way to give a global variable a dynamically scoped temporary value in Python (and Hy doesn't add a macro for it or anything), so you have to do it yourself:

(setv glob 18)

(defn callee []
  (print glob))

(defn nonl [x]
  (callee)
  (global glob)
  (setv old-glob glob)
  (try
    (setv glob x)
    (callee)
    (finally
      (setv glob old-glob)))
  (callee))

(nonl 39)

A macro for this might look like:

(defmacro dyn-rebind-global [symbol new-value #* body]
  (setv temp (hy.gensym))
  `(do
    (global ~symbol)
    (setv ~temp ~symbol)
    (try
      (setv ~symbol ~new-value)
      ~@body
      (finally
        (setv ~symbol ~temp)))))

Then you could use it like this:

(setv glob 18)

(defn callee []
  (print glob))

(defn nonl [x]
  (callee)
  (dyn-rebind-global glob x
    (callee))
  (callee))

(nonl 39)
Kodiologist
  • 2,984
  • 18
  • 33
2

The nearest Python equivalents of dynamic bindings are contextvars and unittest.mock.patch.

patch is mainly intended for unit testing and works on pretty much anything, but is not thread safe. If you need to dynamically rebind something in library code, patch can do it. Use as a context manager or decorator.

>>> from unittest.mock import patch
>>> name = 'Alice'
>>> def greet(): print("Hi", name)
...
>>> greet()
Hi Alice
>>> with patch('__main__.name', 'Bob'):
...     greet()
...
Hi Bob
>>> greet()
Hi Alice

With contextvars it has to be set up as a ContextVar in the first place at the module top level, and has to be explicitly dereferenced with .get() calls, but it works properly in threaded and async code.

If you want to declare your own dynamic variables in Python, it's best to make it a context var when async or threading is a possibility. An asyncio task manager will automatically swap in a new context for each task. You can also do this manually with copy_context. The run() method takes a callable, so it can work as a decorator:

>>> import contextvars as cv
>>> name = cv.ContextVar('name', default='Alice')
>>> def greet(): print('Hi', name.get())
...
>>> greet()
Hi Alice
>>> @cv.copy_context().run
... def _():
...   name.set('Bob')
...   greet()
...
Hi Bob
>>> greet()
Hi Alice

This can all work exactly the same way in Hy, but a macro could make it nicer.

gilch
  • 10,813
  • 1
  • 23
  • 28
  • 1
    These are some interesting ideas, but writing the example code in Hy would be more helpful to readers than writing it in Python. – Kodiologist Mar 29 '22 at 12:42
  • Contextvar is the safest solution in this case, even though it's fun to reimplement dynamic bindings with a macro. This is because Contextvars are already designed to work properly in the context of threads or async concurrency. See [PEP 555](https://peps.python.org/pep-0555/) for more on this subject. Maybe it's possible to come up with a clever macro interface that hides the `ContextVar` boilerplate behind something more akin to Lisp special variables. – shadowtalker Apr 20 '22 at 02:29