Scheme/Guile: самопереопределение переменной внутри функции

Я чувствую, что понимание этой тонкости может помочь мне понять, как область видимости работает в Scheme.

Итак, почему Scheme выдает ошибку, если вы пытаетесь сделать что-то вроде:

(define (func n)
  (define n (+ 1 n))
  n)

Он выдает ошибку только во время выполнения при вызове функции.

Причина, по которой я нахожу это странным, заключается в том, что Scheme допускает переопределение даже внутри функций. Например, это не дает ошибки и всегда будет возвращать значение 5, как и ожидалось:

(define (func n)
  (define n 5)
  n)

Кроме того, Scheme, по-видимому, поддерживает самоопределение в глобальном пространстве. Например:

(define a 5)
(define a (+ 1 a))

не дает ошибок и приводит к тому, что «a» отображает «6», как и ожидалось.

Так почему же одно и то же дает ошибку времени выполнения, когда это происходит внутри функции (которая поддерживает переопределение)? Другими словами: почему самопереопределение не работает только внутри функции?


person ison    schedule 06.09.2017    source источник


Ответы (1)


глобальный define

Во-первых, программы верхнего уровня обрабатываются частью реализации, отличной от функции, и определение уже определенной переменной не допускается.

(define a 10)
(define a 20) ; ERROR: duplicate definition for identifier

Может случиться так, что это работает в REPL, так как переопределение является обычным делом, но при запуске программ это категорически запрещено. В R5RS и до этого то, что произошло, недоопределено и не имеет значения, так как спецификация, нарушая ее, больше не является программой Scheme, и разработчики вольны делать все, что хотят. В результате, конечно, многие недоопределенные вещи получают поведение, зависящее от реализации, которое не является переносимым или стабильным.

Решение:

set! изменяет привязки:

(define a 10)
(set! a 20) 

define в лямбде (функция, пусть, ...)

define в лямбде — это нечто совершенно другое, обрабатываемое совершенно другими частями реализации. Он обрабатывается макросом/специальной формой lambda, поэтому он переписывается в letrec*

letrec* или letrec используются для того, чтобы сделать функции рекурсивными, поэтому имена должны быть доступны во время вычисления выражений. Из-за этого, когда вы define n, он уже затмил n, который вы передали в качестве аргумента. Кроме того, от разработчиков R6RS требуется сигнализировать об ошибке при оценке привязки, которая еще не инициализирована, и, вероятно, именно это и происходит. До того, как разработчики R6RS могли делать все, что хотели:

(define (func n)
  (define n (+ n 1)) ; illegal since n hasn't got a value yet!
  n)

На самом деле это становится:

(define func
  (lambda (n)
    (let ((n 'undefined-blow-up-if-evaluated))
      (let ((tmpn (+ n 1)))
        (set! n tmpn))
      n)))

Теперь компилятор может увидеть, что он нарушает спецификацию во время компиляции, но многие реализации не узнают об этом до запуска.

(func 5) ; ==> 42

Отличный результат в R5RS, если у разработчиков хороший книжный вкус. Разница в версии, которую вы сказали, работает, заключается в том, что это не нарушает правило оценки n перед телом:

(define (func n)
  (define n 5)
  n)

становится:

(define func
  (lambda (n)
    (let ((n 'undefined-blow-up-if-evaluated))
      (let ((tmpn 5)) ; n is never evaluated here!
        (set! n tmpn))
      n)))

Решения

Используйте неконфликтующее имя

(define (func n)
  (define new-n (+ n 1))
  new-n)

Используйте let. У него нет собственной привязки, когда выражение оценивается:

(define (func n)
  (let ((n (+ n 1)))
    n))
person Sylwester    schedule 06.09.2017