I am trying to use a custom replacer function in s-format to enable a string template with this behavior:

(defun transform (x)
  (* 6 x))

(let ((a 5))
  (s-format "${(transform a)}" 'replacer '(())))

Ideally, this would resolve to a string of "30". I want this syntax because it is simpler to me than making a-lists with keys and values.

If I have lexical-binding set to nil that is what happens. But if lexical-binding is set to t, I get an error that the Symbol’s value as variable is void: a.

This is no doubt because I am (mis)-using eval (and maybe using other bad logic) in the replacer function:

(defun replacer (key data)
   ;; sexp
   ((string-prefix-p "(" key)
    (eval (read key)))

   ;; bound variable
   ((and (boundp (intern-soft key))
     (symbol-value (intern-soft key))) 
    (symbol-value (intern-soft key)))

   ;; extract from data
    (or (cdr (assoc key data)) ""))))

The idea in this function is to see the key is an sexp starting with a (, and then read and eval it.That works in a dynamically bound env, but not in a lexically bound one. So, two questions I guess, first, is the string-prefix-p the best way to identify an sexp that could be evaluated? and second given that sexp, how do I reliably evaluate it in the environment it came from? Maybe third, is there some other tried and true way to achieve this goal, or should I just use the very verbose 'aget replacer and stick to a simpler template?

  • Unless there's a way to get access to the lexical environment above the current context, I don't think this is possible. The idea here can be implemented with macros, but I believe s-format as a function doesn't provide the ability to implement this. I'd love to be wrong though. Commented Feb 23 at 14:59
  • You’ve got something like four different questions in there; you should stick to just one question at a time.
    – db48x
    Commented Feb 23 at 15:48
  • I am not sure what is the goal here; or why you could not just use: (let ((a 5)) (format "%d" (transform a))) (you could surround that (transfrom a) with a replacer and/or replace %d with %s if necessary). Could you clear that up? B.t.w. a sexp does not have to start with a ( (see e.g. Parsing Expressions). Commented Feb 23 at 22:12
  • Hey John, returning to this to share an example of how this type of behavior can be built using macros instead of functions. It's not perfect, but this code does what you're trying to do with s-format but with a macro. gist.github.com/jordonbiondo/592618ee665b18205676949f0b3766fa Commented Feb 27 at 16:14
  • And an older one that's better and worse in different ways: gist.github.com/jordonbiondo/c4e22b4289be130bc59b Commented Feb 27 at 17:04

1 Answer 1


(The intention of this response to engage.)

eval always happen within an environment. It is the environment that supplies values to the variables.

In your example, the eval happens in dynamic environment, and the lisp evaluator, when it looks up in its environment, it is not unable to find the value of a variable.

So, the trick is to find out the environment the evaluator is consulting, and to insert the variable and its value in that environment.

In your case, a simple

M-: (setq a 5) RET

will do the job admirably.

(Figure out where the environment in to which above setq puts the value and variable.)

Slightly an elaborate response

eval is a built-in function in C source code.

(eval FORM &optional LEXICAL)

Evaluate FORM and return its value. If LEXICAL is t, evaluate using lexical binding by default. This is the recommended value.

If absent or nil, use dynamic scoping only.

LEXICAL can also represent an actual lexical environment; see the Info node (elisp)Eval for details.

(info "(elisp) Eval")


-- Function: eval form &optional lexical

This is the basic function for evaluating an expression. It evaluates FORM in the current environment, and returns the result. The type of the FORM object determines how it is evaluated.

Note Forms::.

The argument LEXICAL specifies the scoping rule for local variables(*note Variable Scoping::).

  • If it is omitted or nil, that means to evaluate FORM using the default dynamic scoping rule.

  • If it ist, that means to use the lexical scoping rule. The value of LEXICAL can also be a non-empty alist specifying a particular “lexical environment” for lexical bindings; however, this feature is only useful for specialized purposes, such as in Emacs Lisp debuggers. *Note Lexical Binding::.

Since eval is a function, the argument expression that appears in a call to eval is evaluated twice: once as preparation before eval is called, and again by the eval function itself.

(info "(elisp) Lexical Binding")

https://emacsdocs.org/docs/elisp/Lexical Binding

Internally, the lexical environment is an alist of symbol-value pairs, with the final element in the alist being the symbol t rather than a cons cell. Such an alist can be passed as the second argument to the eval function, in order to specify a lexical environment in which to evaluate a form. *Note Eval::. Most Emacs Lisp programs, however, should not interact directly with lexical environments in this way; only specialized programs like debuggers.)

Try the setq or try passing an explicit environment to eval.

(defvar kitchin-env
  '((a . 20)
    (b . 10)

(defun transform (x)
  (* 6 x))

(defun replacer (key env)
   ;; sexp
   ((string-prefix-p "(" key)
    (eval (read key) env))

   ;; bound variable
   ((and (boundp (intern-soft key))
     (symbol-value (intern-soft key))) 
    (symbol-value (intern-soft key)))

   ;; extract from data
    (or (cdr (assoc key env)) ""))))

(s-format "${(transform a)}" 'replacer kitchin-env)

When you do symbol-value what envionment is consulted for the variable's value?

(By now you should have understood that there could be multiple environments, and you can set them up "as you go".

And when you are in a home environment, and say "Wait for x minutes!" you really mean "Wait for 10x or 20x minutes"

... and when you are in a office environment, and say "Wait for x minutes!" (presumably to your boss), you really mean "Wait for less than x minutes" actually. So, context / environment of computation really matters.

  • You can NOT escape providing an alist. The environment--the box that assigns values to variables--IS ITSELF an alist. The question is HOW the alist is inferred and specified. It is you who has to specify the ENVIRONMENT in which the mathematical expression is evaluated. Lisp Evalutor will throw up if it cannot find a value for a variable it wants to evaluate.
    – mxnt
    Commented Feb 24 at 9:55
  • Thanks for the discussion. It looks like it is impossible to do what I was trying unless there is some equivalent to "globals()" in Python for elisp. That is the environment I want to eval in. It isn't possible to anticipate what the alist would be in advance here, and if you have to make one you might as well just use it for the s-format arg anyway. Commented Feb 24 at 15:00
  • Which begs the question, what is the source of the data ... My gut feeling is that you have some babel snippet in your org file (possibly using Python) which gets dumped in the Org file, which you want to use somewhere else. You need to talk about more about source of data ... and not how data is used.
    – mxnt
    Commented Feb 25 at 2:16
  • its not that complicated; it is all in code. The data here comes from an api and is json like api.openalex.org/works/W4210300109. I want to use that data to make a string representation. it isn't a simple use values from the json though, or I wouldn't have this question. I need a function to get an author string for example, and one to generate the abstract. So in the template I want something like ${(get-abstract result)}. Commented Feb 25 at 13:52
  • curl api.openalex.org/works/W4210300109 | jq '.authorships | .[].author.display_name' => "Shanna-Leigh Davidson" "Tagbo H. R. Niepa" You just need to use request-* and shell-command-* and you are done. There is a reason your replacer function is complicated, and but I have hard time imagining what is specific in scenario which makes the replacer look like that.
    – mxnt
    Commented Feb 25 at 14:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.