This is a good point and something Lispers need to constantly keep in mind. CL is a pass by-reference language. I believe most Lisps, including Scheme, are pass by reference for various reasons inherent in basically all computer science, actually (the only exception I can think of is NewLisp). Take a look at discussions regarding equality and generic copying of objects for some insight as to why pass-by-value can be a hairy issue. (Note that I'm not talking about the extreme pass by reference from languages like Fortran/C++ where your variables inside the function are not even scoped to the body of the function. In CL, setting a variable to a value doesn't change the value of the variable in the call frame, in fact you have to use some underhanded tricks to get that to work. By pass by reference, I mean basically the C sense of the phrase. Nothing is copied and modifying constituent parts will change things in other stack frames. Edit: I now remember that C does allow you to pass structures by value (shallow copy) but it is rarely used. I'm thinking of how programmers choose to pass everything that isn't a base type in C by address
This means, as you have correctly noted, that the user must be wary of return values. Particularly, it is the case that if you start mutating bits of returned values, you may cause very odd behavior. For instance, when I was first starting out, it was utterly amazing to me that:
- Code: Select all
* (defun return-list () '(1 2 3))
(1 2 3)
* (let ((list (return-list)))
(setf (second list) -2))
(1 -2 3)
In this case, SBCL is closing over the list '(1 2 3) and when I mutate it, I effectively alter the functions behavior, permanently. I even believe that this is not a predictable feature as a different Lisp implementation might interpret '(1 2 3) to build a new list each time it is executed (though you can get this behavior predictably by explicitly closing over the list). Also, I am basically altering a constant piece of data, which is a no-no and SBCL would have warned me about if it was able to detect it.
A good rule to follow is that unless a function API documentation explicitly tells you it is returning freshly consed (Lisp lingo for allocated) data, you should assume that it is not yours to mutate. An even better rule is, treat lists as immutable data types (don't change values in them, build new ones) and everything else as a mutable type. Really the whole thing is a bit of a hornets' nest but in practice it doesn't bother anyone basically because of these two rules that basically everybody follows.