2

I am trying to convert my code to use lexical binding.

I have a function (format-template) that calls a lambda function (let-bound to replacer-inner) from a user-customizable list (template-replace-functions) of functions based on a match-string. The replacer-inner function takes no arguments and returns a string, however, it may need to use variables foo bar and/or baz, which are passed as part of the argument list to format-template.

(setq lexical-binding t)

(defcustom template-replace-functions
  '(("email"
     (lambda () user-mail-address)
    ("apples"
     (lambda ()
       (cond ((= foo 1)
              "one")
             ((= foo 2)
              "two")
             ((= foo 3)
              "three")
             (t "four"))))
    ("bananas"
     (lambda ()
       (if (eq bar 'move)
           "movement" "sit still")))))
  "Association list of replacement functions.

For each STRING, the corresponding FUNCTION is called with no
arguments and must return a string."
  :type '(repeat (group string function))
  :group 'spice-girls)

(defun format-template (list)
  (let* ((foo (nth 0 list))
         (bar (nth 1 list))
         (baz (nth 2 list))
         template
         (replacer-outer
          (lambda ()
            (replace-regexp-in-string
             "\${\([^\s\t\n]*?\)}"
             (lambda (match)
               (let* ((key (match-string 1 match))
                      (replacer-inner
                       (cadr (assoc key template-replace-functions))))
                 (if (and replacer-inner
                          (stringp (funcall replacer-inner)))
                     (funcall replacer-inner) "")))
             template t t))))
    (setq template
          ...)
    (if template (funcall replacer-outer))))

So, for example, list contains three elements, which are let-bound to foo bar and baz. The key of the match-string is "bananas" which associates to a function in template-replace-functions and sets replace-inner to:

(lambda ()
       (if (eq bar 'move)
           "movement" "sit still"))

At this point, the above lambda function needs to know bar, which is fine using dynamic binding, but not so fine using lexical binding.

My question is, how do I do this to allow the lambda function bound to replacer-inner take from the let-bound values of foo bar and baz?

(Just to confuse things, the replace-outer function, which contains replace-inner, is let-bound because it is called from one of two places in the real function. I could have written inline here but have included it this way in case it adds to the problem.)

Edit:

Halfway there...

(setq lexical-binding t
  foo 0
  bar 0)

(setq inline-fun-1
      '(lambda ()
         (setq return
               (if (eq foo 1)
                   "Pass" "Fail"))))

(defmacro lex-fun ()
  `(let* ((foo 1)
          (bar 1)
          return)
     (funcall ,inline-fun-1)))

(lex-fun)    ; -> "Pass"

(defun inline-fun-2 ()
  (setq return
        (if (eq bar 1)
            "Pass" "Fail")))

(defmacro lex-fun ()
  `(let* ((foo 1)
          (bar 1)
          return)
     (funcall ,inline-fun-2)))

(lex-fun)    ; -> Lisp error: (void-variable inline-fun-2)

So, expanding the lambda function within a macro appears to work, but not a named function. But I do want to allow the user lambda or named functions. How to get around this?

  • 1
    BTW, formally speaking your (lambda () user-mail-address) is not a function. Instead, it's a list of 3 elements, which happens to look the same as the source code for a function. And Emacs is nice enough to turn one into the other if you pass that list to funcall. But if you really want to say that it's a function, then you need to write `(("email" ,(lambda () user-mail-address) ... – Stefan Feb 26 '16 at 21:08
  • @Stefan if the interpreter (Emacs) sees it as a function and the user sees it as a function, then pragmatically speaking, isn't it a function? Will Emacs ever not be nice enough to see it as a function? – rnkn Feb 27 '16 at 4:27
  • 1
    @rnkn It's not a function to Emacs, it's just a list that happens to be converted implicitly in a few situations. But there are notable differences: Lambda lists cannot close over surrounding bindings, and are never byte compiled. They exist purely for historical reasons, not because they are a good idea. You should not rely on this feature in today's Emacs Lisp code. – lunaryorn Feb 27 '16 at 10:11
1

Short answer: you cannot. This is why this kind of binding is called "lexical" - it prevents variables from escaping their definition area.

One way to address this problem: you could have passed some data-structure into the "bananas" function, where "bananas" would know how to extract values from it. I.e.

(defcustom template-replace-functions
  '(("bananas" . (lambda (env) 
                   (let ((x (gethash "x" env "")))
                     (do-replacements-with x))))
    ...)))

Which assumes that env is a hash-table.

As an aside: it's better to use eql rather than eq. I don't think Emacs Lisp gives any useful guarantees as to how eq behaves with almost any type of data (eg. I'm not sure that even interned symbols with the same name must be equal under eq).

  • 2
    I cast a down vote for the last remark about eq. Are you aware that eq is exactly equivalent for all data types except of numeric ones? Besides, two uninterned symbols aren't the same even if they share the same name, and hence must not be equal under eq. – lunaryorn Feb 26 '16 at 21:25
  • @lunaryorn if you read what I wrote it says "I'm not sure that even interned symbols with the same name must be equal under eq". I said nothing about uninterned symbols. My reluctance to using eq is due to Common Lisp, where this is almost never a good thing to do. I don't know the implementation details of Emacs Lisp, so, it could be just a superstition on my side. Yet I'd still avoid it: you never know, maybe future Emacs Lisp will be more like Common Lisp? – wvxvw Feb 26 '16 at 21:58
  • 1
    With all due respect, Emacs Lisp is not Common Lisp, nor ever will be. You might as well avoid lists because they might be removed when Emacs Lisp gets more like C. Please remove the last paragraph from your answer; it's confusing and misleading. Interned symbols by the way are equal under eq, simply because there's only a single obarray in Emacs Lisp. – lunaryorn Feb 26 '16 at 22:11
  • I can and I will! I've edited the question to include a partial solution using a macro, but it's still not a full solution. – rnkn Feb 27 '16 at 4:30
  • The problem with passing env to the custom functions is that it requires the user to add a dummy argument to their functions, as they won't necessarily know the values of foo bar and baz, and I feel like that's not user-friendly enough. However this has made me reconsider passing an unknown lexical environment to the user. Could be bad. – rnkn Feb 27 '16 at 4:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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