Originally posted on zwitterio.it
This post is intended to give beginners (or anyone else) an introduction on how to use lisp to manage user's inputs on Emacs.
A simple interactive function
Imagine you want to write a simple interactive function to cheer up an Emacs' user.
(defun cheer-me-up ()
(interactive)
(message "You're great, keep it up!"))
(cheer-me-up)
Everytime we launch this one, the same result will be showed in the minibuffer:
"You’re great, keep it up!"
It works, but it can be boring on the long run.
Enhance the function by proposing a new catchphrase every time you launch it.
(defun cheer-me-up (goodvibes)
(interactive "sEnter good vibes as string: ")
(message goodvibes))
This way, the function will reflect your communication and optimism on yourself, by repeating exactly the expression you pass.
Every time the function will be launched by the user, the minibuffer of Emacs will ask for good vibes.
That's pretty basic, because we're just asking for a string and rethrowing it to the user without any check.
But what if the user isn't writing good stuff on the first place?
Maybe he needs this function precisely because it's all melancholical and gloomy.
Demand good vibes
We could introduce a check that excludes bad words from the input and replace it with good ones, but it wouldn't be easy.
Doable, but not easy.
Let's keep it simple for the moment: what if we ask the user something else?
Not a phrase, for example, but a number?
We could list a great number of positive phrases and ask the user for a number, which will be the index of the phrase in the list.
In this case, the input should be an integer.
How do we do that?
First of all, let's declare a list named goodvibes
:
;; list of positive phrases
(setq goodvibes
'("You're great!" "Keep it up!" "People loves you!"
;; It's better to comment out the following ones
;; "Shut up, nothing you say has value" "The world is an insane fabric of pain and we should destroy everything"
;; Ok, we can keep the last one
"Nature is good, she's not a cruel mother and you must be happy to be alive. Of course."))
Now we have a total of 4 elements in the list and we want to access them from the function.
The function asks for an index, which should be an integer between 0 and 3.
What if the user doesn't know anything about the list up here and asks for a fifth element?
What if the user gives us a float number insted of an integer? Or, worst, what if the user writes off a numberless string?
(defun cheer-me-up (index)
(interactive "sChoose a number, receive good vibes~ ")
;; We are asking for a string from the user, so this could be anything.
;; We must be sure it's an integer.
<<format-input-as-integer>>
;; Isn't it possible to convert the string to an integer?
;; Tell the user!
<<signal-no-number-error>>
;; Now we need to know the integer is not inferior to 0 or superior to 3
(if (and (> index 0) (<= index 3))
;; if we pass the condition, good, print out your message
(message (nth index goodvibes))
;; Else block:
;; if the number exceeds the range from a way or another, tell the user.
(<<signal-out-of-range-error>>)))
To manage those cases, we need to know two concepts:
- How to format strings (here in the GNU docs);
- How to signal errors (here in the GNU docs);
You might say:
But reading documentation is bOrInG!
Don't worry, this time we will explore the topics practically here below; just follow me through the comments.
Formatting strings
Formatting as integer is pretty simple if you know how to format strings. From the docs:
Formatting means constructing a string by substituting computed values at various places in a constant string.
In this case, we want to require that the value is of a particular type and it can be checked specifying a format. %d
, for example,
replace the specification with the base-ten representation of a signed integer. The object can also be a floating-point number that is formatted as an integer, dropping any fraction.
So, if the user gives us 1
, we're fine. And what if the user gives us 1.0
? We're fine too! The floating point will be converted automatically to the closest integer. This means that 1.1
or 1.2
will be both converted to 1
too. In other context, you could manage those cases differently, because if the user is giving you floating points, maybe the decimal means something and you want to tell them something's wrong. This case is simple, so we will be chill about that.
;; assign the parsed value to the index var itself;
;; we're not going to need the raw input anymore;
(setq index (format "%d" index))
Of course, if the user gives us something that cannot be in any way interpreted as an integer, we have to tell them. To be honest, this part is already managed by the format
command itself.
If you supply a value that doesn’t fit the requirements, an error is signaled.
So, we don't have to implement this code by ourselves:
;; If user's input is not a number, the `format` function will tell them;
;; We can proceed with our function.
Throwing an error
We still cannot vibe with our function results because another block, the last one, is missing.
What if the index is not in the desired range?
Of course, an error would be displayed this time too, but we need to explain clearly to the user what they have to do to not triggering it.
Signaling an error means beginning error processing. Error processing normally aborts all or part of the running program and returns to a point that is set up to handle the error (see How Emacs Processes Errors).
Errors are managed by error
function (what a shock, isn't it?). A typical use of error from the docs:
(error "That is an error -- try something else")
error→ That is an error -- try something else
We take inspiration for our use-case:
(error "Out of range error -- choose a number between 0 and 3.")
As nv-elisp points out on Reddit,
When using message you'll want to provide the format-string argument. Otherwise the user's strings will fail if they include format specifiers. e.g. if your goodvibes list includes "%dont mind me%", the %d will be interpreted as part of the format-string argument and will throw an error.
For an error caused by user input, it's better to use user-error. This will prevent the debugger from being hit any time a user inputs an out of range number.
So, in this case it would be more advisable to write user-error
instead of simply error
:
(user-error "Out of range error -- choose a number between 0 and 3.")
Wrapping up
We learned how to use format
and error
, two fundamental functions to manage user's inputs or for writing lisp for Emacs in general.
In particular, we should remember that:
-
error
stops the process and throws up the specified error to the user; -
format
can have various specifications like:-
%s
, for common strings; -
%d
, for integers; -
%f
, for "floats".
-
To be absolutely correct, I should remember you that there's not a single type of integers or floats or strings; for example, strictly abiding to what the docs are saying, %f
refers to "the decimal-point notation for a floating-point number"; you can store floats in other ways, if you need.
Coming back to our function, that's how it finally appears:
(defun cheer-me-up (index)
(interactive "sChoose a number, receive good vibes~ ")
;; We are asking for a string from the user, so this could be anything.
;; We must be sure it's an integer.
;; assign the parsed value to the index var itself;
;; we're not going to need the raw input anymore;
(setq index (format "%d" index))
;; Isn't it possible to convert the string to an integer?
;; Tell the user!
;; If user's input is not a number, the `format` function will tell them;
;; We can proceed with our function.
;; Now we need to know the integer is not inferior to 0 or superior to 3
(if (and (> index 0) (<= index 3))
;; if we pass the condition, good, print out your message
(message (nth index goodvibes))
;; Else block:
;; if the number exceeds the range from a way or another, tell the user.
((user-error "Out of range error -- choose a number between 0 and 3."))))
This is the moment for a confession: in order to teach how the format
function works, I somehow have disguised you. The interactive
function, in fact, gives the chance to filter the values in place! That's what the "s" before "Choose" is there for (see Code Characters for interactive in the docs to learn more)
Knowing this, we could write everything more concisely, like that:
(defun cheer-me-up (index)
(interactive "nChoose a number, take good vibes~ ")
(if (and (> index 0) (<= index 3))
(message (nth index goodvibes))
((user-error "Out of range error -- choose a number between 0 and 3."))))
I started with the "n" character, because it means you want to take a number as an argument.
The only difference from before is that this new function lacks the previous tolerance for floats, but I don't see this as a flaw.
Watch out!
format
specifications andinteractive
first character's meanings are not exactly superimposable!
Using "f" ininteractive
means asking for a file, not for a float.
Keep it up!
Hoping this post was an enjoyable passage in your path through lispian parenthesis, I wish you good luck for your journey.
(nth 1 goodvibes)
;; => "Keep it up!"
(and a big thanks to Arialdo for his help in tracking down my typos)
Top comments (0)