Writing a destructive macro or function like incf?
Asked Answered
H

2

3

I need an incf function which does some bounds checking during the increment:

val := val + delta
if val >= 1.0
   then return 1.0
   else return val

I can write this using incf:

(defun incf-bounded(val delta)
  (incf val delta)
  (if (>= val 1.0) 1.0 val))    

In such case I need to use this like (setf x (incf-bounded x delta)). But how do I write one which I can use like (incf-bounded x delta), i.e., where x will be modified?

Hypostasis answered 21/10, 2013 at 3:48 Comment(3)
This might be a duplicate of what is to append as push is to cons, in Lisp?, the accepted answer to which explains the use of define-modify-macro, which is what you'd probably want to use here.Wight
The lisp code you've written doesn't match your pseudo-code. Shouldn't the else line be else val := val + delta?Wight
Possible duplicate of defining setf-expanders in Common LispAsclepiadean
W
5

This is a good use case for define-modify-macro (which has also been described in what is to append as push is to cons, in Lisp?, but the present case is simpler). First, write your bounded sum as a function. This is pretty straightforward; it takes val and delta and returns 1.0 if their sum is greater than 1.0, and their sum otherwise. Based on the pseudo code and Lisp code you posted, this could be:

(defun sum-bounded (val delta)
  (if (>= (+ val delta) 1.0)
      1.0
      (+ val delta)))

Actually, for just computing this value, you can use:

(defun sum-bounded (val delta)
  (min 1.0 (+ val delta)))

Now you use define-modify-macro to define a macro incf-bounded:

(define-modify-macro incf-bounded (delta) sum-bounded)

The macro takes a place as its first argument and delta as a second. It safely retrieves the value from the place, computes sum-bounded with that value and delta, and then stores the result back into the place. “Safely” here means that it avoids possible problems with multiple evaluation, as Lars Brinkhoff's wisely warns against. Then you just use it:

(let ((x .5))
  (incf-bounded x .3)
  (print x)                             ; prints 0.8
  (incf-bounded x .3)
  (print x))                            ; prints 1.0 (not 1.1)

For more complicated cases where the place that would be modified isn't naturally the first argument to the macro that you want, you'd need to write your own macro and use get-setf-expansion, but this is explained in more detail in

Code all together for easy copy & paste

(defun sum-bounded (val delta)
  "Returns the lesser of 1.0 or the sum of val and delta."
  (min 1.0 (+ val delta)))

(define-modify-macro incf-bounded (delta) sum-bounded
  "(incf-bounded place delta) computes the sum of the value of the
place and delta, and assigns the lesser of 1.0 and the sum of the value
and delta to place.")

(defun demo ()
  (let ((x .5))
    (incf-bounded x .3)
    (print x)                           ; prints 0.8
    (incf-bounded x .3)
    (print x)))                         ; prints 1.0 (not 1.1)
Wight answered 21/10, 2013 at 14:59 Comment(0)
G
4

You may want to be careful about val, if you want it to be a place which can have side effects:

(defmacro incf-bounded (form delta &environment env)
  (multiple-value-bind (temps vals vars writer reader)
      (get-setf-expansion form env)
    `(let* (,@(mapcar #'list temps vals)
            (,(first vars) (min (+ ,delta ,reader) 1.0))) ;Edited, see comments.
       ,writer)))

Try it with e.g.

(let ((list (list 0 0.5 1)))
  (loop with i = -1 repeat 3 do (incf-bounded (nth (incf i) list) 0.5))
  list)

(This looks needlessly complicated, because I wanted a side effect in the first argument to incf-bounded.)

Greenwood answered 21/10, 2013 at 5:29 Comment(3)
Speaking of needlessly complicated, why not just use define-modify-macro, which handles the get-setf-expansion for you? :) (Shameless plug: I put an example in https://mcmap.net/q/1479676/-writing-a-destructive-macro-or-function-like-incf .)Wight
Yes, that can be convenient. Myself, I quite like to use get-setf-expension.Greenwood
To work correctly, the (+ ,reader ,delta) form should be (+ ,delta ,reader) CLHS 5.1.3 Treatment of Other Macros Based on SETF.Townsman

© 2022 - 2024 — McMap. All rights reserved.