问题
I hope this isn't beating a dead horse, but I'd like an opinion about another possible strategy for writing referentially transparent code. (The previous discussion about referential transparency is at Using a Closure instead of a Global Variable). Again, the objective is to eliminate most global variables, but retain their convenience, without injecting bug-prone references or potentially non-functional behavior (ie, referential opaqueness, side-effects, and non-repeatable evaluations) into the code.
The proposal is to use local special variables to establish initial bindings, which can then be passed dynamically to the subsequent nested functions that eventually use them. The intended advantage, like globals, is that the local special variables do not need to be passed as arguments through all the intermediate functions (whose functionality has nothing to do with the local special variables). However to maintain referential transparency, they would be passed as arguments to the final consumer functions.
What I'm wondering about is whether floating a lot of dynamic variables around is prone to programming bugs. It doesn't seem particularly error prone to me, since any local rebinding of a previously bound variable should not affect the original binding, once it is released:
(defun main ()
(let ((x 0))
(declare (special x))
(fum)))
(defun fum ()
(let ((x 1)) ;inadvertant? use of x
(setf x 2))
(foo))
(defun foo ()
(declare (special x))
(bar x))
(defun bar (arg) ;final consumer of x
arg)
(main) => 0
Are there problems with this stragegy?
回答1:
Now your functions are referencing a variable that is not guaranteed to be defined. Trying to execute (foo)
at the repl will throw an unbound variable error. Not only is there referential opacity, but now referential context error throwing!
What you have here are globally bound routines, which can only be executed in the local context where (declare (special x))
has been hinted. You may as well put those functions in a labels
so they don't get accidentally used, though at that point you are choosing between closing the variables in functions, or closing the functions in a function:
(defun main ()
(labels ((fum ()
(let ((x 1));Inadvertent use of x?
(setf x 2))
(foo))
(foo ()
(declare (special x))
(bar x))
(bar (arg) arg)) ;Final consumer of x.
(let ((x 0))
(declare (special x))
(fum))))
Wow, that is some ugly code!
After a convolution we can make x
lexical! Now we can achieve the holy grail, referential transparency!
Convolute
(defun main ()
(let ((x 0))
(labels ((fum ()
(let ((x 1))
(setf x 2))
(foo))
(foo () (bar x))
(bar (arg) arg));Final consumer of x.
(fum))))
This code is much nicer, and lispy. It is essentially your code to the other question, but the functions bindings are localized. This is at least better than using explosive global naming. The inner let does nothing, same as before. Though now it is less convoluted.
CL-USER> (main) ;=> 0
Your test case is the same (main) ;=> 0
in both. The principle is to just encapsulate your variables lexially instead of with dynamic special
declarations. Now we can reduce the code even more by just passing things functionally in a single environment variable, as suggested.
(defun convoluted-zero ()
(labels ((fum (x)
(let ((x 1))
(setf x 2))
(foo x))
(foo (x) (bar x))
(bar (arg) arg)).
(fum 0)))
CL-USER> (let ((x (convoluted-zero)))
(list x (convoluted-zero)))
;=> 0
□ QED your code with the special variables violates abstraction.
If you really want to go down the rabbit hole, you can read the section of chapter 6 of Doug Hoyte's Let Over Lambda on pandoric macros, where you can do something like this:
(use-package :let-over-lambda)
(let ((c 0))
(setf (symbol-function 'ludicrous+)
(plambda () (c) (incf c)))
(setf (symbol-function 'ludicrous-)
(plambda () (c)(decf c))))
You can then use pandoric-get
to get c without incrementing it or defining any accessor function in that context, which is absolute bonkers. With lisp packages you can get away with a package-local "global" variable. I could see an application for this in elisp, for example, which has no packages built in.
来源:https://stackoverflow.com/questions/56762865/using-a-local-special-variable-passed-as-a-final-argument