6

STRUCTURED EDITING

Writing Lisp code often involves moving forms around or wrapping forms in new forms. If you're using lispy-mode or lispyville-mode–both turned on with Doom's lispy config in the init.el file–you can more easily build out and modify Lisp code.

SLURPING AND BARFING

Slurping and barfing are terms for describing moving parentheses to either include or exclude forms from other forms. Let me show you what I mean.

In the tic-tac-toe project, we wrote code to find positions on the board to win or block the opponent's win. To begin, we wrote this:

(find-if (lambda (triplet) (= 20 (sum-triplet *test-board* triplet)))
 *triplets*)

This finds any triplet that sums to 20 on the board. But we then wanted to search the result of that find-if form to search for the empty space in the triplet.

(find-if (lambda (element) (= (nth element board) 0))
         (find-if (lambda (triplet) (= 20 (sum-triplet *test-board* triplet)))
 *triplets*))

If you use structural editing, this can actually be difficult at first. You might naturally try to write the code like this:

(find-if (lambda (element) (= (nth element board) 0)))
(find-if (lambda (triplet) (= 20 (sum-triplet *test-board* triplet)))
 *triplets*)

And then, in order to wrap the bottom find-if form with the top find-if form, you might try to delete the final parenthesis of the top find-if and then move it to the end of the bottom find-if form:

(find-if (lambda (element) (= (nth element board) 0)) ; deleted paren
(find-if (lambda (triplet) (= 20 (sum-triplet *test-board* triplet))) *triplets*)) ; added
                                                                                     ; paren

What actually happens, however, is that the entire top find-if form is deleted when you try to delete just the last parenthesis! This very confusing behavior is from lispy-mode. It is designed to keep parentheses balanced at all times.

If you want to move only one parenthesis, then, what do you do?

One option would be to kill the find-if form intended to become the inner form…

(find-if (lambda (element) (= (nth element board) 0)) ; bottom form killed

and then paste it at the end of the outer find-if

(find-if (lambda (element) (= (nth element board) 0)) (find-if (lambda (triplet) (=
 20 (sum-triplet *test-board* triplet))) *triplets*))

and then move the cursor to the beginning parenthesis of the inner find-if and press ENTER to place it on a new line.

(find-if (lambda (element) (= (nth element board) 0))
         (find-if (lambda (triplet) (= 20 (sum-triplet *test-board* triplet)))
 *triplets*))

This is okay, but there is an even easier method called slurping.

Start with this code:

(find-if (lambda (element) (= (nth element board) 0)))
(find-if (lambda (triplet) (= 20 (sum-triplet *test-board* triplet)))
 *triplets*)

Place the cursor on the last parenthesis of the top form. While still in Evil Normal mode (not Insert mode), press > (the lispyville-mode keybinding for lispyville-slurp). The parenthesis will move to the end of the bottom find-if form, making it an inner nested form.

Slurping is especially useful for any time you need to "build out" from some code. Consider this:

(find-if (lambda (triple)
             (= (sum-triple *test-board* triple) target-sum))
         *triples*)

This is how we started building out the code for detecting squeezes. But then we realized that we needed to add more conditions. For a strategy to be a squeeze, the triplet need to sum to the target, and it needs to be diagonal as well as some other conditions. That means that we needed to wrap several predicate functions in an and form.

;; These need to be wrapped in an (and ...) form
(= (sum-triple board triple) target-sum)
(diagonal-p triple)

We can do that by first writing the and form before the other two forms:

(and) (= (sum-triple board triple) target-sum)
(diagonal-p triple)

Put the Evil Normal-mode cursor on the end paren of the and form, and then slurp up the other two forms.

(and (= (sum-triple board triple) target-sum)
     (diagonal-p triple))

Once we added the rest of the conditions, we wanted to then bind the return value of the find-if form to a local variable using let. The process looks like this:

;; Write the LET and variable we want in the LET.
(let ((squeeze-p))) (find-if (lambda (triple)
             (and (= (sum-triple board triple) target-sum)
                  (diagonal-p triple)
                  (not (human-in-middle-p board))
                  (side-empty-p board)))
         *triples*)

Then use lispyville-slurp on the three ending parentheses of the let form to complete the new form.

After adding an if form, we need to make this a function, so we can write the function definition above the entire let form:

(defun detect-squeeze (board target-sum))
(let ((squeeze-p
        (find-if (lambda (triple)
                     (and (= (sum-triple board triple) target-sum)
                          (diagonal-p triple)
                          (not (human-in-middle-p board))
                          (side-empty-p board)))
                 *triples*)))
  (when squeeze-p
      (find-empty-position board *sides*)))

And then just slurp up the entire let into the defun form.

Again, you could just kill and paste the forms around, but killing can sometimes be dangerous (if you kill a word using M-backspace, you've just replaced your function in the kill-ring). It's nicer to simply keep the code on the screen and using slurp to do what you want.

The reverse of slurp is barf, bound to < in Doom Emacs lispyville-mode. I don't find it nearly as useful as slurping, but maybe you can find a use for it.

DRAGGING FORMS FORWARD/BACKWARD

Another operation you'll probably want to do a lot is move forms around. For example, maybe you wrote this:

(defun choose-move (board)
  (or (make-three-in-a-row board)
      (block-opponent-win board)
      (take-random-position board)
      (block-two-on-one-play board)
      (block-squeeze-play board)))

Then you realized that the block-two-on-one and block-squeeze-play strategies need to take higher priority over all but the make-three-in-a-row strategy. You could, of course, kill/paste them into the proper position. But if you want, you can also use lispyville-drag-backward–bound to M-k–while the cursor is on the opening parentheses of the block- forms to move them. lispyville-drag-forward–bound to M-j–goes in the opposite direction.

They also work on individual symbols. For example, let's say you have a call to nth:

(nth 4 my-list)

But then you decide you want to use member instead.

(member 4 my-list)

Unfortunately, you now need to switch the location of 4 and my-list in the arguments to member. Fortunately, you can use lispyville-drag- functions for that, too. Place the cursor over the 4 and use lispyville-drag-forward (M-j) to switch the 4 and my-list.

MORE RESTRUCTURING & NAVIGATING TOOLS

Remember that you can get more information about the keybindings available in the buffer using either M-x describe-key (SPC h k), M-x describe-mode (SPC h m), and M-x embark-bindings (SPC h b b). Look into the lispy-, lispyville-, and special-lispy- commands and their keybindings.