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?

+3


source to share


4 answers


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 ...

+5


source


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 .

+1


source


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.

+1


source


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

    and room

    that use*standard-output*

  • describe

    which can use either *standard-output*

    or*terminal-io*

  • trace

    / untrace

    and time

    that use*trace-output*

  • dribble

    which can bind *standard-input*

    and / or*standard-output*

  • step

    and inspect

    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.

+1


source







All Articles