Search code examples
design-patternscoding-stylecommon-lispanti-patternsdynamic-scope

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?


Solution

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