A major pitfall is that the binding semantics for undefined variables—i.e. variables not defined with defvar
and friends—change with lexical-binding
: Without it, let
binds everything dynamically, but with lexical-binding
enabled undefined variables are bound lexically, and even elided completely if unused in the current lexical scope.
Old code sometimes relies on this. To avoid hard dependencies for optional features, it'd bind dynamic variables without requiring the corresponding library or declaring the variable itself:
(let ((cook-eggs-enabled t))
(cook-my-meal))
If the cooking feature is optional, we don't want to force unnecessary dependencies onto the user, so we don't use (require 'cook)
and instead rely on autoloading of the cook-my-meal
function.
It's obvious for the human reader that cook-eggs-enabled
is not a local variable, but still refers to some global dynamic variable from the cook
library here. Without lexical-binding
this code works as intended: cook-eggs-enabled
is bound dynamically, whether defined or not.
With lexical-binding
however, it breaks: cook-eggs-enabled
is now bound lexically (and then optimized away, because it's not used), so the global dynamic variable cook-eggs-enabled
is not ever touched at all and still nil
by the time cook-my-meal
is called, so we surprisingly won't have any eggs in our meal.
Luckily, these issues are very easy to spot: The byte compiler naturally warns about an unused lexical binding here.
The fix is simple: Either add a (require 'cook)
(for features that aren't really optional anyway), or—to avoid hard dependencies—declare the variable as dynamic variable in your own code. There is a special defvar
form for this:
(defvar cook-eggs-enabled)
This defines cook-eggs-enabled
as dynamic variable, but doesn't affect the docstring, the load-history
(and thus find-variable
and friends) or anything else, except the binding nature of the variable.