Daniel Jour
Daniel Jour

Reputation: 16156

lisp: dynamic scope vs explicit parameter passing

I see two different patterns for "output" 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 using dynamic scope like in implicit considered bad practice or is this a generally accepted use of dynamic scoping? Note that I'm assuming this is for e.g. a DSL to build complex output, like HTML, SVG, Latex or whatever and is not expected to do anything different apart from producing a printed representation.

Are there - apart from style - any important differences, e.g. with respect to performance, concurrency, or whatever?

Upvotes: 3

Views: 374

Answers (4)

acelent
acelent

Reputation: 8135

The only statistically relevant pattern I see in Common Lisp itself, other than explicitly passing a stream, is an optional stream argument which defaults to *standard-input* or *standard-output* depending on the direction that the function requires.

The implicit cases in Common Lisp all deal with unspecified input/output, such as:

  • y-or-n-p/yes-or-no-p which use *query-io*

  • apropos, disassemble and room which use *standard-output*

  • describe which can use either *standard-output* or *terminal-io*

  • trace/untrace and time which use *trace-output*

  • dribble which may bind *standard-input* and/or *standard-output*

  • step and inspect may do whatever they want, from nothing, to a standard-input and standard-output command loop, to displaying a graphical tool window

So, I believe all other cases you might have seen are from libraries. My advise is to not follow any implicit pattern. However, one good exception is in HTML generators, which bind some variable e.g. *html-stream* so that the macros that follow can refer to that variable without clutter. Imagine if you had to tell 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 (with-html-output) and AllegroServe's HTML generator.

So, the advantage here is purely syntatic.

There is never a performance reason to use dynamic bindings. There might be a stack space reason, to avoid passing a stream as an argument, but this is a very weak reason, any existent recursion will just blow a little bit further.

Upvotes: 1

Throw Away Account
Throw Away Account

Reputation: 2671

I just wanted to add that one thing you can do in Common Lisp is combine the two practices:

(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 rebinds the dynamic variable *standard-output* to the value of that argument.

Upvotes: 1

Rainer Joswig
Rainer Joswig

Reputation: 139251

Actually you can bind *standard-output* directly:

(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, this makes debugging easier. They appear on the argument lists and are easier to spot in the backtrace. Otherwise you would need to see in the backtrace where there is a dynamic rebinding of a stream variable.

a) Nothing to pass?

(defun print-me (&optional (stream *standard-output*))
  ...)

b) One or more fixed arg:

(defun print-me-and-you (me you &optional (stream *standard-output*))
  ...)

c) One or more fixed args and multiple optional args:

(defun print-me (me
                 &key
                 (style  *standard-style*)
                 (font   *standard-font*)
                 (stream *standard-output*))
  ...)

Note also this:

Now assume (implicit) has an error and we get a break loop, a debugging repl. What's the value of standard-output in this break 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 is what you see in LispWorks or SBCL. Here you have access to the real program's binding, but using output functions during debugging will have effects on this stream.

In other implementations *standard-output* will be rebound to actual terminal io - for example in Clozure CL and CLISP.

If your program does not rebind *standard-output* there is less confusion in those cases. If I write code, I often think about what would be more useful in a REPL environment - which is different from languages, where there is less interactive debugging on REPLs and break loops...

Upvotes: 5

Augusto
Augusto

Reputation: 29847

I'm not a Lisp expert, but I've seen plenty of code using implicit values for *standard-output*. The argument from the lisp community is that this approach makes the code easier to run/test in the REPL (I come from a C/Java background so anything that smells of a global variable makes feel unease, but it's the lisp way).

About concurrency, each thread in CL has a different copy of *standard-output*, so your threads will be safe, but you need to configure them properly. You can read a bit more about this in the lisp cookbook - threads section.

Upvotes: 1

Related Questions