0

Programming Perl in cperl-mode I often use _ symbol, so I decided to remap it with / symbol, so I could type faster. I created the following functions:

(defun add-underscore ()
  (interactive)
  (insert "_"))

(defun add-backslash ()
  (interactive)
  (insert "/"))

(defun my-cperl-mode ()
  "Perl mode customisations."
  (local-set-key (kbd "/") 'add-underscore)
  (local-set-key (kbd "_") 'add-backslash)
)

(add-hook 'cperl-mode-hook 'my-cperl-mode)

Then I decided to merge similar functions into one and created this:

(defun output-symbol (symbol)
  (interactive)
  (insert symbol))

(defun my-cperl-mode ()
  "Perl mode customisations."
  (local-set-key (kbd "/") (funcall output-symbol ?\_))
  (local-set-key (kbd "_") (funcall output-symbol ?\/))
)

Unfortunately, it doesn't work. What is wrong?

3
  • FYI - This: (local-set-key (kbd "/") 'add-underscore) is the same as this: (local-set-key (kbd "/") "_").
    – Drew
    Sep 15 at 21:43
  • @Drew But you can't transpose keys that way: the macros would call each other indefinitely. Sep 15 at 22:00
  • @Gilles: Yes, if you use two like that. I was speaking generally.
    – Drew
    Sep 15 at 22:10
1

In most Lisp dialects, including Emacs Lisp, symbols have a value as a variable which is independent of their function definition. (funcall output-symbol …) refers to the variable-value of output-symbol, not to the function. You could refer to the function instead by passing the symbol itself to funcall: if you pass a symbol to funcall, it uses that symbol's function definition. This means quoting the symbol: (funcall 'output-symbol …). But this won't work because…

You run (funcall …) at the time you define the key binding. This is pointless. (funcall 'output-symbol ?_) is just a complicated way of writing (output-symbol ?_). (The reason funcall exists is when you want to call a function reference stored in a variable.) You want to delay the call to output-symbol, so you need to put it inside a function — you need to add a layer of function abstraction, not remove one: (local-set-key (kbd "/") (lambda () (output-symbol ?_))). But this won't work because…

A key can only be bound to a command, i.e. a function that has an interactive specification. A function that calls a function that has an interactive specification isn't good enough. You need (local-set-key (kbd "/") (lambda () (interactive) (output-symbol ?_))). You don't really save anything with the output-symbol function, but it does work.

By the way, the interactive specification in the definition of output-symbol is useless: it specifies that output-symbol receives zero arguments when called interactively, so an interactive call would fail.

Before I go further, let me mention that calling local-set-key in a mode hook is weird: you're doing the keymap customization each time the mode starts. This is weird but ok for your own use; it wouldn't be acceptable in a package because you'd be overriding the user's customizations. The normal way to customize key bindings in a particular mode is to use define-key on that mode's keymap. Do this after the cperl module is loaded, by registering a function with eval-after-load.

(defun my-cperl-mode-customizations ()
  (define-key cperl-mode-map (kbd "_") (lambda () (interactive) …))
  )
(eval-after-load "cperl-mode" '(my-cperl-mode-customizations))

You can avoid repeating the interactive specification by using a function that builds a command, instead of using an auxiliary function.

(defun make-insert-command (text)
  (lexical-let ((text text))
    #'(lambda ()
        (interactive)
        (insert text))))

(defun my-cperl-mode-customizations ()
  (define-key cperl-mode-map (kbd "_") (make-insert-command "/"))
  (define-key cperl-mode-map (kbd "/") (make-insert-command "_"))
  )
(eval-after-load "cperl-mode" '(my-cperl-mode-customizations))

The make-insert-command command isn't very nice. Here are a few ways to improve it:

  • Add * to the interactive specification so that the command errors out if the buffer is read-only.
  • Add p to the interactive specification to allow inserting the character multiple times with a prefix argument. The function needs a count argument to receive the prefix argument.
  • Call undo-auto-amalgamate so that typing foo_bar will be a single undo action, as it is when _ is an ordinary key.
(defun make-insert-command (text)
  (lexical-let ((text text))
    #'(lambda (&optional n)
        (interactive "*p")
        (undo-auto-amalgamate)
        (or n (setq n 1))
        (let ((i 0))
          (while (< i n)
            (setq i (1+ i))
            (insert text))))))
(defun my-cperl-mode-customizations ()
  (define-key cperl-mode-map (kbd "_") (make-insert-command "/"))
  (define-key cperl-mode-map (kbd "/") (make-insert-command "_"))
  )
(eval-after-load "cperl-mode" '(my-cperl-mode-customizations))

This is still not very nice because the binding for _ and / will appear as some unreadable Lisp code in help. Anonymous commands are possible, but for this reason (as well as other reasons that don't matter here — some aspects of interactive commands depend on the command symbol's property list, and that requires the command to be a symbol bound to an interactive function and not directly an interactive function).

2
  • Unfortunately the call to lambda, generated by make-insert-command gives an error: funcall-interactively: Symbol’s value as variable is void: text. Seems, that inside lambda we can't see the definition of text parameter.
    – user4035
    Sep 19 at 5:29
  • I changed make-insert-command to the working version. This is related to the lexical binding of symbols, which is required for creating closures. stackoverflow.com/questions/593383/…
    – user4035
    Sep 19 at 12:16
1
  1. You're missing a quote: (funcall 'output-symbol ?\_)

    If you want to call the symbol-function of the symbol output-symbol then you need to pass that symbol. To do that, quote it, so evaluating returns the symbol.

  2. But you don't need to do that. To call a function whose name you already have, just call it directly: (output-symbol ?\_).

  3. But that's no good, because you need to bind a key to a command, that is, (1) a function that (2) has an interactive spec.

(defun output-symbol (symbol)
  (insert symbol))

(defun my-cperl-mode ()
  "Perl mode customisations."
  (local-set-key (kbd "/") (lambda () (interactive) (output-symbol ?\_)))
  (local-set-key (kbd "_") (lambda () (interactive) (output-symbol ?\/))))

Or just what you had in the beginning. Or just

(defun my-cperl-mode ()
  "Perl mode customisations."
  (local-set-key (kbd "/") (lambda () (interactive) (insert ?_)))
  (local-set-key (kbd "_") (lambda () (interactive) (insert ?/))))

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.