Lisp: dynamic scope with explicit parameter passing
I see two different templates for "exit" functions in (common) lisp:
(defun implicit ()
(format t "Life? Don't talk to me about life!"))
(defun explicit (stream)
(format stream "This will all end in tears."))
(defun test-im-vs-ex-plicit ()
(values
(with-output-to-string (stream)
(let ((*standard-output* stream))
(implicit)))
(with-output-to-string (stream)
(explicit stream))))
Is it implicit
considered bad practice to use dynamic scope, or is it a common use of dynamic scope? Note that I am assuming this is for, for example, a DSL to create complex output like HTML, SVG, Latex, etc. and is not expected to do anything other than create a printable representation.
Are there any important differences besides the style, for example. in terms of performance, concurrency or something else?
source to share
You can actually link directly *standard-output*
:
(defun test-im-vs-ex-plicit ()
(values
(with-output-to-string (*standard-output*) ; here
(implicit))
(with-output-to-string (stream)
(explicit stream))))
There is no real simple answer. My advice:
Use stream variables, it makes debugging easier. They appear in argument lists and are easier to spot in the opposite direction. Otherwise, you will need to see in the backtrace where there is dynamic recovery of the thread variable.
a) Nothing will work?
(defun print-me (&optional (stream *standard-output*))
...)
b) One or more fixed arg arguments:
(defun print-me-and-you (me you &optional (stream *standard-output*))
...)
c) One or more fixed arguments and more optional arguments:
(defun print-me (me
&key
(style *standard-style*)
(font *standard-font*)
(stream *standard-output*))
...)
Note this also:
Now suppose we (implicit)
have a bug and we get a break loop, debug repl. What is the value of standard output in this aborting loop?
CL-USER 4 > (defun test ()
(flet ((implicit ()
(write-line "foo")
(cerror "go on" "just a break")
(write-line "bar")))
(with-output-to-string (stream)
(let ((*standard-output* stream))
(implicit)))))
TEST
CL-USER 5 > (compile 'test)
TEST
NIL
NIL
CL-USER 6 > (test)
Error: just a break
1 (continue) go on
2 (abort) Return to level 0.
3 Return to top loop level 0.
Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.
CL-USER 7 : 1 > *standard-output*
#<SYSTEM::STRING-OUTPUT-STREAM 40E06AD80B>
CL-USER 8 : 1 > (write-line "baz")
"baz"
CL-USER 9 : 1 > :c 1
"foo
baz
bar
"
Above you see in LispWorks or SBCL. Here you have access to the actual binding of the program, but using output functions while debugging will have implications for that thread.
Other implementations *standard-output*
will recover to the actual terminal io - for example, in Clozure CL and CLISP.
If your program doesn't recover *standard-output*
, there is less confusion in these cases. If I write code, I often think about what would be more useful in a REPL environment, which is different from languages where less interactive debugging on the REPL and loop breaks ...
source to share
I'm not a Lisp expert, but I've seen a lot of code using implicit values for *standard-output*
. The Lisp community's argument is that this approach makes the code easier to run / test in the REPL (I'm coming from a C / Java background, so anything that smells like a global variable is a concern, but it's the Lisp way).
About concurrency, every thread in CL has a different copy *standard-output*
, so your threads will be safe, but you need to set them up correctly. You can read a little about this in the lisp cookbook - threads section .
source to share
I just wanted to add that one thing you can do in Common Lisp combines two methods:
(defun implicit (&optional (message "Life? Don't talk to me about life!"))
(format t message))
(defun explicit (*standard-output*)
(implicit "This will all end in tears."))
Since *standard-output*
is the name of the argument, calling explicit
with a stream argument automatically restores the dynamic variable *standard-output*
to the value of that argument.
source to share
The only statistically significant pattern I see in Common Lisp itself, other than an explicitly passed stream, is the optional stream argument, which defaults to *standard-input*
or *standard-output*
depending on the direction the function wants.
Implicit cases in Common Lisp all refer to unspecified I / O, for example:
-
y-or-n-p
/yes-or-no-p
that use*query-io*
-
apropos
,disassemble
androom
that use*standard-output*
-
describe
which can use either*standard-output*
or*terminal-io*
-
trace
/untrace
andtime
that use*trace-output*
-
dribble
which can bind*standard-input*
and / or*standard-output*
-
step
andinspect
can do anything from nothing to stdin and standard output loop to display the graphical tool window
So, I believe all the other cases you may have seen are libraries. My advice is not to follow any implicit pattern. However, one good exception is HTML generators that bind some variable, for example. *html-stream*
so that subsequent macros can refer to this variable without interference. Imagine if you needed to specify the stream on each macro (not a real example):
(html
(head (title "Foo"))
(body (p "This is just a simple example.")
(p "Even so, try to imagine this without an implicit variable.")))
For real examples, check out (at least) CL-WHO (c-html-output) and AllegroServe HTML Generator .
So the advantage here is purely syntactic.
There is never a reason to use dynamic bindings. There might be a stack space reason to avoid passing a stream as an argument, but that is a very weak reason, any existing recursion will just hit a little further.
source to share