Can a redefinition of a standard procedure change the behavior of other standard procedures?

Viewed 110

Suppose we redefine the standard procedure car using one of the following:

(define (car)
  (display "vroom vroom!"))

or

(set! car
  (lambda ()
    (display "vroom vroom!")))

Is it possible that in a standards compliant Scheme implementation, redefining car in this manner would affect the behavior of other standard procedures that happen to be defined using car?

For example, is a Scheme implementation allowed to internally define cadr as (define (cadr x) (car (cdr x))) such that a redefinition of car by the user at the REPL would change the behavior of cadr at the REPL?

3 Answers

No. The first example ((define (car) ...) binds "car" to a new location, shadowing the standard binding. In implementation uses of "car" ((define (cadr x) ...), "car" has its original binding.

(set! car ..., with "car" having its standard binding, must signal an error:

% scheme
Chez Scheme Version 9.5.7.6
Copyright 1984-2021 Cisco Systems, Inc.

> (display car)
#<procedure car>
> (set! car cdr)
Exception: attempt to assign immutable variable car

Search for "immutable" in a standard report, eg r6rs: "All explicitly exported variables are immutable in both the exporting and importing libraries." (So the base library cannot export a mutable car)

Yes. The behavior of the full system can be changed. But not how you did.

% mit-scheme
MIT/GNU Scheme running under GNU/Linux
....

1 ]=> (pe)

;Value: (user)

1 ]=> (ge system-global-environment)

;Package: ()
;Value: #f

1 ]=> (pe)

;Value: ()

1 ]=> (define + 1)

;Value: +

Move back to user environment (which is the same environment where initial REPL environment points to).

1 ]=> (ge user-initial-environment)

;Package: (user)
;Value: #[environment 12]

1 ]=> (pe)

;Value: (user)

1 ]=> (+ 1 2) ; no more

;The object 1 is not applicable.
;To continue, call RESTART with an option number:
; (RESTART 2) => Specify a procedure to use in its place.
; (RESTART 1) => Return to read-eval-print level 1.

2 error> (assoc 'car 
           (environment-bindings
             system-global-environment))

;Value: (car #[compiled-procedure 1351
          ("list" #x1) #x1c #xe0f67c])

So you can change the CAR (which is defined in system-global-environment) in the same way I changed the +.

But, of course, this is a bad idea, your system won't work any more.

There are some internal procedures that are using car or +, which are compiled. Those that are compiled won't make reference to the system-global-environment and continue to work. Those that are not compiled will crash. Now, depends on how your system was booted, to see which ones make reference to the system env. If some library procedures are loaded in the system without compilation, it is for sure to crash.

Note that when I say "compilation", it means in general other way of interpretation other than calling the interpret/eval function, it means to bypass the standard interpreter. If you use a byte-compiler whose virtual machine makes reference to system environment, that procedure compiled as bytecode will crash. If the code is fully compiled to x86 or mips, it has its own referenced to standard library procedures written in assembler and it won't make reference to global environment. Everything is possible in a scheme system.

If you re-define the procedure in the user-environment (as you did), the system will continue working, as the standard library procedures point to the system environment and no system procedure will see the user environment. But all the procedures that you load in the user-environment will use your own definition of car. In this case, it is okay to redefine the things.

Note that the command (load "xxx") has an optional argument called environment and you can use this option to tell load what environment to change by loading to. By default, in batch-mode and REPL mode, load will write into the user-environment whose parent is system-env. But you can do tricks as the library load-option does, and manipulate environments into a very complex way (a library that creates modules).

There are different answers for different revisions.

R5RS and earlier:

You are not allowed to define existing bindings. Thus your first attempt is not correct Scheme according to the report and the result of running it can be anything. Eg. a segfault is ok, signaling an error is ok, ignoring it is ok.

You are only allowed to set a top level binding, but it needs to be backwards compatible with the original binding. Eg. It is not allowed to define car the way you do, but this is allowed:

(let ((orig-car car))
  (set! car 
        (lambda (o)
          (if (vector? o)
              (vector-ref o 0)
              (orig-car o)))))

(car '(1 2 3))       ; ==> 1
(car (vector 1 2 3)) ; ==> 1

The implementation is free to constant fold and compile. Thus in reality it might never use your new version, but the report does not stop the implementation from implementing cadr the way you wrote it and will then use the new implementation of car. As long as you keep to the standard and make it compatible the result is predictable.

R6RS and later:

From R6RS we have libraries and we can rename in and out and overriding it will not leak over in other libraries. Thus you can set! car, but it will not leak over so that cadr will start using it even if that is the way the implementation did cadr. This voids the need for it to be backwards compatible as well.

Usually you can make your own list library and in you code refrain from using the standard but rather your new version and it will use that. The link is rather static (you import explicit) so the bindings are never confused with the standard library ones.

Related