Я считаю, что лучший способ думать об этом - думать с точки зрения привязок , а не сред или фреймов, которые являются просто контейнерами для привязок.
Привязки
Связывание - это связь между name и value .Имя часто называют «переменной», а значение - это значение переменной.Значением привязки может быть любой объект, о котором язык может говорить вообще.Привязки, однако, являются закулисными вещами (иногда это называется «не быть объектами первого класса»): это не вещи, которые могут быть представлены в языке, а скорее вещи, которые вы можете использовать как часть моделио том, как работает язык.Так что значение привязки не может быть привязкой , поскольку привязки не являются первоклассными: язык не может говорить о привязках.
Есть несколько правил для привязок:
- существуют формы, которые их создают, из которых наиболее важными являются:
lambda
и define
; - - не первоклассные привязки - язык не может представлять привязкив качестве значений;
- привязки являются или могут быть изменяемыми - вы можете изменить значение привязки, как только она существует - и форма, которая делает это, является
set!
; - нет оператора, который уничтожает привязку;
- привязки имеют лексическую область действия - привязки, доступные для фрагмента кода, это те, которые вы можете увидеть, посмотрев на негоне те, которые вы должны догадаться, запустив код и которые могут зависеть от динамического состояния системы;
- только одна привязка для данного имени всегда доступна из данного бита кода - если больше, чемодин виден лексически, тогда самый внутреннийимеет любые внешние;
- привязки имеют неопределенный экстент - если привязка когда-либо доступна для фрагмента кода, она всегда доступна для него.
Очевидно, что эти правила должны быть значительно разработаны (особенно в отношении глобальных привязок и привязок с прямой ссылкой) и могут быть формальными, но этого достаточно, чтобы понять, что происходит.В частности, я на самом деле не думаю, что вам нужно тратить много времени на беспокойство о средах: среда небольшого количества кода - это просто набор привязок, доступных для него, поэтому вместо беспокойства об окружающей среде просто беспокойтесь о привязках.
Вызов по значению
Итак, что означает «вызов по значению», так это то, что при вызове процедуры с аргументом, который является переменной (привязкой), передается ей значение привязки переменной, а не самой привязки.Затем процедура создает привязку new с тем же значением.Из этого вытекают две вещи:
- оригинальная привязка не может быть изменена процедурой - это следует из того, что процедура имеет только значение, а не само связывание, и привязки не являются первыми.класс, поэтому вы не можете обмануть, передав саму привязку в качестве значения;
- , если само значение является изменяемым объектом (массивы и conses являются примерами объектов, которые обычно являются изменяемыми, числа являются примерами объектов, которые являютсянет) тогда процедура может мутировать этот объект.
Примеры правил привязки
Итак, вот несколько примеров этих правил.
(define (silly x)
(set! x (+ x 1))
x)
(define (call-something fn val)
(fn val)
val))
> (call-something silly 10)
10
Итак, здесь мы создаем две привязки верхнего уровня для silly
и call-something
, каждая из которых имеет значения, которые являются процедурами.Значение silly
- это процедура, которая при вызове:
- создает новую привязку с именем
x
и значением которой является аргумент silly
; - изменяет эту привязку, поэтому ее значение увеличивается на единицу;
- возвращает значение этой привязки, которое на единицу больше, чем значение, с которым она была вызвана.
Значение call-something
- это процедура, которая при вызове:
- создает две привязки, одну с именем
fn
и одну с именем val
; - вызывает значение привязки
fn
со значением привязки val
; - возвращает значение привязки
val
.
Обратите внимание, что независимо от того, вызов fn
делает, он не может изменить привязку val
, потому что не имеет к нему доступа.Итак, что вы можете знать , взглянув на определение call-something
, это то, что, если он вообще вернется (он может не вернуться, если вызов fn
не вернется), он вернетзначение его второго аргумента.Эта гарантия означает то, что означает «вызов по значению»: язык (такой как Fortran), который поддерживает другие механизмы вызова, не всегда может это обещать.
(define (outer x)
(define (inner x)
(+ x 1))
(inner (+ x 1)))
Здесь есть четыре привязки: outer
- этопривязка верхнего уровня, значением которой является процедура, которая при вызове создает привязку для x
, значением которой является ее аргумент.Затем он создает другую привязку с именем inner
, значением которой является другая процедура, которая при вызове создает привязку new для x
к ее аргументу , а затем возвращаетзначение этой привязки плюс один.outer
затем вызывает эту внутреннюю процедуру со значением ее привязки для x
.
Здесь важно то, что в inner
есть две привязки для x
, которые потенциально лексически видимы, но самый близкий - тот, который установлен inner
- побеждает, потому что только одна привязка для данного имени может быть когда-либо доступна за один раз.
Вот предыдущий код (это не будетэквивалентно, если inner
был рекурсивным) выражается с явным lambda
s:
(define outer
(λ (x)
((λ (inner)
(inner (+ x 1)))
(λ (x)
(+ x 1)))))
И, наконец, пример мутирующих привязок:
(define (make-counter val)
(λ ()
(let ((current val))
(set! val (+ val 1))
current)))
> (define counter (make-counter 0))
> (counter)
0
> (counter)
1
> (counter)
2
Итак, make-counter
(имя привязки, значением которой является процедура, которая при вызове устанавливает новую привязку для val
, а затем возвращает созданную ею процедуру.Эта процедура создает новую привязку с именем current
, которая перехватывает текущее значение val
, изменяет привязку для val
, чтобы добавить единицу, и возвращает значение current
.В этом коде используется правило «если вы когда-либо видите привязку, вы всегда можете ее увидеть»: привязка для val
, созданная при вызове make-counter
, видима для процедуры, которую она возвращает, пока существует эта процедура (и эта процедура существует, по крайней мере, до тех пор, пока существует привязка к ней), и она также изменяет привязку с set!
.
Почему не среды?
SICP , в главе 3 , вводится «модель среды», в которой в любой точке существует среда, состоящая из последовательности кадров, каждый из которых содержит привязки.Очевидно, что это прекрасная модель, но она вводит три вида вещей - среду, рамки в среде и привязки в рамке - две из которых совершенно нематериальны.По крайней мере, для привязки вы можете получить ее каким-то образом: вы можете видеть, как она создается в коде, и вы можете видеть ссылки на нее.Поэтому я предпочитаю не думать об этих двух дополнительных вещах, с которыми вы никогда не сможете справиться.
Однако это выбор, который не имеет разницы на практике: думать исключительно с точки зрения привязокПомогает мне, если думать с точки зрения окружения, рамки и привязки вполне могут помочь другим людям.
Сокращения
В дальнейшем я буду использовать сокращение для разговоров о привязках, особенно top-.Уровни:
- '
x
- это процедура, которая ...' означает 'x
- это имя привязки, значением которой является процедура, которая при вызове ...'; - '
y
is ...' означает 'y
- имя привязки, значение которой равно ...'; - '
x
вызывается с помощью y
'означает' значение привязки, названной x
, вызывается со значением привязки, названной y
'; - ' ... binding
x
to ... 'означает'... создает привязку с именем x
и значением ...'; - '
x
' означает 'значениеx
'; - и т. д.
Распространение описаний привязок является обычным делом, так как полностью явный способ просто болезнен: я пытался (но, возможно, местами не получилось) быть полностью явным выше.
Ответ
И наконец, после этой длинной преамбулы, вот ответ на вопрос, который вы задали.
(define (make-withdraw balance)
(λ (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds")))
make-withdraw
связывает balance
со своим аргументом и возвращает процедуру, которую он делает.При вызове этой процедуры:
- связывает
amount
со своим аргументом; - сравнивает
amount
с balance
(который он все еще может видеть, потому что он мог видеть его, когда онбыл создан); - если денег достаточно, он мутирует привязку
balance
, уменьшая ее значение на значение привязки amount
и возвращает новое значение; - , если естьнедостаточно денег, которые он возвращает
"Insuficient funds"
(но не изменяет привязку balance
, поэтому вы можете повторить попытку с меньшей суммой: реальный банк, вероятно, высосет часть денег из привязки balance
на данный момент в качестве штрафа).
Now
(define x (make-withdraw 100))
создает привязку для x
, значение которого является одной из процедур, описанных выше: в этой процедуре balance
изначально 100
.
(define (f y) (y 25))
f
- это процедура (это имя привязки, значением которой является процедура, которая при вызове) связывает y
с ее аргументом и затем вызываетэто с аргументом 25
.
(f x)
Итак, f
это вызовредактирование с x
, x
является (связано) с процедурой, построенной выше.В f
, y
привязан к этой процедуре (не к ее копии, к ней), и эта процедура затем вызывается с аргументом 25
.Затем эта процедура ведет себя так, как описано выше, и результаты выглядят следующим образом:
> (f x)
75
> (f x)
50
> (f x)
25
> (f x)
0
> (f x)
"Insufficient funds"
Обратите внимание, что:
- никакие первоклассные объекты не копируются нигде в этом процессе:«копия» процедуры не создана;
- никакие первоклассные объекты не видоизменены где-либо в этом процессе;
- привязки не создаются (и впоследствии становятся недоступными и поэтому могут быть уничтожены) в этом процессе;
- в этом процессе неоднократно мутируется одна привязка (один раз для каждого вызова);
- Мне нигде не нужно упоминать «среды», которые представляют собой просто набор привязок, видимых из определенноготочка в коде, и я думаю, что не очень полезная концепция.
Я надеюсь, что это имеет какой-то смысл.
Более сложная версия приведенного выше кода
Что-то, что вы можете захотеть сделать, это отменить транзакцию на вашем счету.Один из способов сделать это - вернуть, как и новый баланс, процедуру, которая отменяет последнюю транзакцию.Вот процедура, которая делает это (этот код находится в Racket ):
(define (make-withdraw/backout
balance
(insufficient-funds "Insufficient funds"))
(λ (amount)
(if (>= balance amount)
(let ((last-balance balance))
(set! balance (- balance amount))
(values balance
(λ ()
(set! balance last-balance)
balance)))
(values
insufficient-funds
(λ () balance)))))
Когда вы создаете учетную запись с помощью этой процедуры, то при ее вызове возвращается два значения: первое - этоновый баланс или значение insufficient-funds
(по умолчанию "Insufficient funds"
), вторая процедура, которая отменит транзакцию, которую вы только что сделали.Обратите внимание, что это отменяет его, явно возвращая старый баланс, потому что вы не можете полагаться на (= (- (+ x y) y) x)
, являющуюся истинным в присутствии арифметики с плавающей точкой, я думаю.Если вы понимаете, как это работает, вы, вероятно, понимаете привязки.