7

Say I define a buffer-local variable foo, and its default value is "a":

(defvar foo "a")
(make-variable-buffer-local 'foo)
(default-value 'foo) ;; => "a"

Immediately after this I run the following code:

(let ((foo "b"))
  (with-temp-buffer
    (message "foo: %s" foo))) ;; => "b"

The result is "b", which is the value I set in let.

Now if I use setq to set the variable, then rerun the exact same code as before:

(setq foo "c") ;; => "c"

(let ((foo "b"))
  (with-temp-buffer
    (message "foo: %s" foo))) ;; => "a"

The result is "a", which is the default value now.

The question: for a temporary buffer, the default value of foo is not set until I use setq? And as long as I don't use setq, I can use let to change the default value in other buffers?

EDIT: as @npostavs said, this is what make-varible-buffer-local really means. If I use make-variable-buffer-local myself, I can always use setq after that. But this becomes really tricky for the "built-in" buffer-local variables like case-fold-search. if I, as a package author, bind case-fold-search to nil in the outer let, and I want to use the default value (it might or might not be set by the user) in the with-temp-buffer, I have to use setq before with-temp-buffer to make sure the default value is actually being used in case the user doesn't have that setq in his/her init.el. For buffer-local variables, it probably means setq is always safer than let when we want to set the value. I wonder whether the design or the documentation could be improved.

cutejumper
  • 787
  • 5
  • 12
  • If you put the let-bound portion inside the body of `with-temp-buffer` (instead of prior thereto), does that help any? `with-temp-buffer` is a macro and it behaves a little differently than a standard function. E.g., `(with-temp-buffer (let ((foo "b")) (message "foo: %s" foo)))` – lawlist Feb 19 '17 at 04:54
  • @lawlist That should work as expected, which I understand. But I can't find any documentation explaining the example I said in the question. – cutejumper Feb 19 '17 at 04:58
  • I would probably ask the question -- How can I penetrate the body portion of a macro with a previously let-bound variable (e.g., with a back-tick/comma approach, or lexical-let, or perhaps something else)? ... But, I don't want to hijack your question ... a macro maven should be along shortly to explain it all ... :) – lawlist Feb 19 '17 at 05:00
  • 1
    @lawlist I don't know whether this is related to the macro. I actually test it locally using the expanded form of `with-temp-buffer` (that said, no macros). I think it is more like a specific behavior for buffer-local variables. – cutejumper Feb 19 '17 at 05:05

2 Answers2

6

I, as a package author, bind case-fold-search to nil in the outer let, and I want to use the default value (it might or might not be set by the user) in the with-temp-buffer,

In this case I would recommend

(let ((case-fold-search (progn (make-local-variable 'case-fold-search)
                               nil)))
  (with-temp-buffer
    ...))

The important thing to note here is that make-variable-buffer-local does not make a variable buffer-local! It only arranges for it to become buffer-local after it is set. You might want to use make-local-variable instead.

(make-variable-buffer-local VARIABLE)

Make VARIABLE become buffer-local whenever it is set.

The second thing to note is the interaction of let with buffer-local variables, from (elisp) Intro to Buffer-Local:

Warning: When a variable has buffer-local bindings in one or more buffers, let rebinds the binding that's currently in effect. For instance, if the current buffer has a buffer-local value, let temporarily rebinds that. If no buffer-local bindings are in effect, let rebinds the default value.

So in the first case you bind the global value to "b" and that shows up in the temp buffer. In the second case, after you have set the local value in *scratch* to "c", you then bind the local value to "b", but other buffers still see the global value of "a".

npostavs
  • 9,033
  • 1
  • 21
  • 53
  • I guess I misunderstood the sentence `Make VARIABLE become buffer-local whenever it is set.`. I thought it was saying whenever we set the value, only the buffer-local value is set. So to avoid such a problem, we probably should always use `setq` immediately after `make-variable-buffer-local`? – cutejumper Feb 19 '17 at 05:28
  • I actually encounter this problem for "built-in" buffer-local variables like `case-fold-search` and `fill-column`. Emacs itself doesn't set them after it defines these buffer-local variables, and it requires user to set them in order to make them really buffer-local. Is this design intentional? I don't think most people aware of this. – cutejumper Feb 19 '17 at 05:47
  • Requiring the user to use `make-local-variable` or `setq` while it says this variable is a buffer-local is really confusing. But maybe that is another question regarding the design. – cutejumper Feb 19 '17 at 05:56
  • Well, if it was made immediately buffer local, then the question would be how to let-bind the default value at all... – npostavs Feb 19 '17 at 06:07
  • Thanks. I still feel weird for using `make-local-variable` when I see the variable is already "buffer-local" from the manual. I would expect the `let` could bind the buffer-local copy. But this may be more suitable to be asked in Emacs-devel. – cutejumper Feb 19 '17 at 07:20
  • @npostavs letf on default-value might work in that case. – YoungFrog Feb 19 '17 at 07:20
  • To make things more ..hmm.. interesting, some variables are always buffer-local without needing to `set` them (e.g. `default-directory`). This is a messy part of Elisp's semantics. – Stefan Nov 03 '18 at 15:24
0

Like your proposed solution, we can bind this to an arbitrary variable inside let, then change as you wish while remaining inside let, before reverting back to the original value.

The problem arises when changing buffer inside let - due to dynamic scoping, the value of local variables will not 'carry-over' into the new buffer, even when inside lexical-let.

Here's a function which checks for this problem and also sets and reverts the default value, which is the one used by a temporary buffer:

(defun f1 (VAR TEMP-VALUE) "
Sets VAR to TEMP-VALUE; works with local variables also." 
   (interactive)
   ;; starting value
   (insert (prin1-to-string VAR))
   (let ((var1 VAR))
     (setq VAR TEMP-VALUE)
     (when (local-variable-p 'VAR)
       (setq-default VAR TEMP-VALUE))
     ;; current buffer-local value
     (insert (prin1-to-string VAR))
     ;; value in new buffer
     (with-temp-buffer
       (message (prin1-to-string VAR)))
     ;; back to current buffer - unchanged
     (insert (prin1-to-string VAR))
     ;; revert
     (setq VAR var1)
     (when (local-variable-p 'VAR)
       (setq-default VAR var1))
   ;; back to original value
   (insert (prin1-to-string VAR)))
   ;; exit let and re-check
   (insert (prin1-to-string VAR)))

then

(f1 case-fold-search "xyz") --> t"xyz""xyz"tt

and "xyz" will appear in the *Messages* buffer.

Other local variables that come to mind in this context are default-directory and uniquify-managed...

In short, you could define a function like this and use it inside let, so save the hassle of checking whether the variable (symbol) is local or not:

(defun set-ld (SYM VAL) "set local (and default if applicable)"
       (interactive)
       (setq SYM VAL)
       (when (local-variable-p 'SYM)
     (setq-default SYM VAL)))
dardisco
  • 189
  • 2
  • 6