Continuing the discussion of less familiar parts of Lisp for newcomers from C++, we’ll discuss the notinline declaration a bit. This pairs up with inline, of course, but we’ve saved this discussion until now because of the context I wanted to introduce first, particularly as relates to symbols and the manipulation of their function cells.
By now, you’ve noticed that Lisp function calls involve execution-time symbol resolution. While you’re probably familiar with load-time linking in C++ programs, things are a bit different in Lisp, because it is legal to change the function definition associated with a symbol, even while a program is running. Perhaps, then, you’ve wondered about inline functions in Lisp. How can you inline a function when the very definition of the function might change from one call to the next?
In C++, the compiler has fairly broad discretion on inlining functions. The programmer may also hint that inlining is desirable for a particular function, with the inline keyword. Some compilers recognize options that ask them to inline any function that is “reasonable” to inline, subject to the implementor’s definition.
In Lisp, the inline declaration tells the system that it is allowed and encouraged to inline this function, until cancelled by a notinline declaration. Note that the Lisp implementation is free to ignore this, and is not required to have the ability to inline. If a function attached to an interned symbol is inlined, and the function definition is later changed, those areas where the old form was inlined are not changed. This can lead to confusion in an active development context, or when manipulations of the symbol table are made, such as a package import that shadows the function in question. One way to avoid this confusion is to try to confine your use of inline to non-interned functions such as those created with labels or flet.
Here, then, is a transcript showing the effect of inlining:
CL-USER> (declaim (inline function-1))
; No value
CL-USER> (defun function-1 ()
(format t "This is the old function-1~%"))
FUNCTION-1
CL-USER> (defun function-2 ()
(format t "In function-2, calling by symbol:~%~T")
(funcall 'function-1)
(format t "In function-2, calling by function:~%~T")
(funcall #'function-1))
FUNCTION-2
CL-USER> (declaim (notinline function-1))
; No value
CL-USER> (function-2)
In function-2, calling by symbol:
This is the old function-1
In function-2, calling by function:
This is the old function-1
NIL
CL-USER> (setf (fdefinition 'function-1)
(lambda ()
(format t "This is the new function-1~%")))
#<FUNCTION (LAMBDA ()) {10036FEE7B}>
CL-USER> (function-2)
In function-2, calling by symbol:
This is the new function-1
In function-2, calling by function:
This is the old function-1
NIL
You can see that function-1 was declared inlineable, and then we defined function-2, which invokes function-1. I invoke function-2, and nothing unexpected happens. Then, I change the function bound to the function-1 symbol (see the earlier article on fdefinition if this is unclear). Now, when I invoke function-2, I get both the old and new forms of function-1 appearing. Why is that? If you recall our discussion at fboundp, the single-quote prefix tells funcall, at run-time, to look up the function bound to the symbol. On the other hand, the #’ prefix tells the reader to replace the content with the function expansion of the symbol. Since the function is inlineable at the time the reader sees this form, the definition of function-1 is inserted into the code at that point, and persists even if that definition is later modified or deleted. So, this is confusing, try not to do it.