Writing a destructive macro or function, such as incf?

I need an incf function that checks some bounds during 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 that case, I need to use this as (setf x (incf-bounded x delta)) . But how can I write one that I can use, for example, (incf-bounded x delta) , i.e. Where will x be changed?

+3
macros lisp common-lisp
source share
2 answers

This is a good use case for define-modify-macro (which has also been described in what you need to add as push to minus in Lisp?, But this case is simpler). First write your limited amount as a function. It is pretty simple; it takes val and delta and returns 1.0 if their sum is greater than 1.0 , and their sum otherwise. Based on the pseudocode and Lisp code you posted, it could be:

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

Actually, for a simple calculation of this value, you can use:

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

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

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

The macro takes place as the first argument and delta as the second. It safely retrieves the value from the location, computes the sum-bounded with that value and the delta, and then saves the result back to the location. โ€œSafeโ€ means that this avoids the potential for multiple rating problems, as Lars Brinkhoff 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) 

In more complex cases where the place to be changed is not the natural first argument for the macro you want, you need to write your own macro and use get-setf-expansion , but this is described in more detail in

  • what needs to be added as push to minus in Lisp?

Code to simplify copying and pasting

 (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) 
+5
source share

You can be careful about val if you want this to be a place that may 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 for example

 (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 unnecessarily complicated because I need a side effect in the first argument to incf-bounded .)

+4
source share

All Articles