When calling a function we commonly pass one or more parameters to that function. In most cases these are simple variables; integers or floats, but could be lists or arrays. In the case of passing arrays there is a hidden danger.
Let’s consider a simple function:
1 2 3 |
(defun product (x y) (* x y)) |
To use this function we pass two values, x and y, and the function returns the product.
1 2 3 4 |
(setf val1 21) (setf val2 2) (print (product val1 val2)) ;returns 42 |
Nothing surprising so far, and if we look at our variables there is nothing surprising there either:
1 2 3 |
(print val1) ;returns 21 (print val2) ;returns 2 |
Now let’s try something similar passing arrays as arguments. For this example we will pass two arrays of equal length, and multiply each ‘nth’ value of one array with the ‘nth’ value of the other:
1 2 3 4 5 6 7 8 9 10 11 |
(defun product (x y) (dotimes (i (length x) x) (setf (aref x i) (* (aref x i) (aref y i))))) (setf a1 #(1 2)) (setf a2 #(3 3)) (setf a1 (product a1 a2)) (print a1) ;returns #(3 6) (print a2) ;returns #(3 3) |
Everything is still as expected, but let’s now make one slight change:
1 2 3 4 5 6 7 8 9 10 11 |
(defun product (x y) (dotimes (i (length x) x) (setf (aref x i) (* (aref x i) (aref y i))))) (setf a1 #(1 2)) (setf a2 #(3 3)) (setf a1 (product a2 a1)) (print a1) ;returns #(3 6) (print a2) ;returns #(3 6) |
Note the final line. Why / how did the value a2 change? Other than the initialization there is no obvious indication that a2 will change.
The answer to this mystery is in understanding SETF.
The ‘setf’ special form evaluates the field ‘placeN’ and sets ‘exprN’ as it’s value.
In our function ‘product’, we did not only set the elements of a locally contained ‘x’, we set the evaluated elements of ‘x’. In other words, Nyquist looks up what ‘x’ is and sees that it is array ‘a1’, and then sets the value of each of its elements. Thus, not only is the array ‘x’ changed, but also it’s parent ‘a1’ is changed.
Not only does this provide opportunities to create difficult to find bugs, but raises the question “how do we prevent this from happening?” If we want to pass an array as an argument to a function and we do not want the value of the array to change, how do we ensure that it doesn’t?
The obvious solution might seem to be that we just need to ensure that ‘x’ is local to the function – perhaps something like:
1 2 3 4 5 |
(defun product (val1 val2) (let ((x val1) (y val2)) (dotimes (i (length x) x) (setf (aref x i) (* (aref x i) (aref y i)))))) |
However, that does not work – it is not a solution. When the variable ‘x’ is evaluated it still points back to our original ‘a1’ array, and the result is the same – ‘a1’ is modified:
1 2 3 4 5 6 7 8 9 10 11 12 |
(defun product (val1 val2) (let ((x val1) (y val2)) (dotimes (i (length x) x) (setf (aref x i) (* (aref x i) (aref y i)))))) (setf a1 #(1 2)) (setf a2 #(3 3)) (setf a1 (product a2 a1)) (print a1) ;returns #(3 6) (print a2) ;returns #(3 6) |
To get this to work in the way that we want (not changing ‘a1’), we must create a new array that is local to the function. We can then safely set the values of its elements and use it to provide the return value:
1 2 3 4 5 6 7 8 9 10 11 12 |
(defun product (x y &aux (z (make-array (length x)))) (dotimes (i (length x) z) (setf (aref z i) (* (aref x i) (aref y i))))) (setf a1 #(1 2)) (setf a2 #(3 3)) ;; This works as expected (setf a1 (product a1 a2)) (print a1) ;returns #(3 6) (print a2) ;returns #(3 3) |