Rất nhanh chóng: một sự thay thế là "minh bạch tham chiếu" nếu "thay thế như dẫn đến thích" và một chức năng là "thuần túy" nếu tất cả các hiệu ứng của nó được chứa trong giá trị trả về của nó. Cả hai đều có thể được làm chính xác, nhưng điều quan trọng cần lưu ý là chúng không giống nhau và thậm chí không có nghĩa là cái kia.
Bây giờ hãy nói về việc đóng cửa.
Nhàm chán (chủ yếu là thuần túy) "đóng cửa"
Việc đóng cửa xảy ra bởi vì khi chúng tôi đánh giá một thuật ngữ lambda, chúng tôi diễn giải các biến (bị ràng buộc) là các tra cứu môi trường. Do đó, khi chúng tôi trả về một thuật ngữ lambda là kết quả của việc đánh giá các biến bên trong nó sẽ "đóng lại" các giá trị mà chúng nhận được khi nó được xác định.
Trong phép tính lambda đơn giản, đây là loại tầm thường và toàn bộ khái niệm chỉ tan biến. Để chứng minh rằng, đây là một trình thông dịch tính toán lambda tương đối nhẹ:
-- untyped lambda calculus values are functions
data Value = FunVal (Value -> Value)
-- we write expressions where variables take string-based names, but we'll
-- also just assume that nobody ever shadows names to avoid having to do
-- capture-avoiding substitutions
type Name = String
data Expr
= Var Name
| App Expr Expr
| Abs Name Expr
-- We model the environment as function from strings to values,
-- notably ignoring any kind of smooth lookup failures
type Env = Name -> Value
-- The empty environment
env0 :: Env
env0 _ = error "Nope!"
-- Augmenting the environment with a value, "closing over" it!
addEnv :: Name -> Value -> Env -> Env
addEnv nm v e nm' | nm' == nm = v
| otherwise = e nm
-- And finally the interpreter itself
interp :: Env -> Expr -> Value
interp e (Var name) = e name -- variable lookup in the env
interp e (App ef ex) =
let FunVal f = interp e ef
x = interp e ex
in f x -- application to lambda terms
interp e (Abs name expr) =
-- augmentation of a local (lexical) environment
FunVal (\value -> interp (addEnv name value e) expr)
Phần quan trọng cần chú ý là addEnv
khi chúng ta gia tăng môi trường bằng một tên mới. Hàm này chỉ được gọi là "bên trong" của Abs
thuật ngữ lực kéo được giải thích (thuật ngữ lambda). Môi trường được "tra cứu" bất cứ khi nào chúng ta đánh giá một Var
thuật ngữ và vì vậy những người đó Var
quyết tâm với bất cứ điều gì Name
được đề cập đến trong Env
đó bị bắt bởi Abs
lực kéo có chứa Var
.
Bây giờ, một lần nữa, trong điều khoản LC đơn giản, điều này là nhàm chán. Nó có nghĩa là các biến bị ràng buộc chỉ là hằng số như bất cứ ai quan tâm. Họ được đánh giá trực tiếp và ngay lập tức như các giá trị mà họ biểu thị trong môi trường theo phạm vi từ vựng cho đến thời điểm đó.
Đây cũng là (gần như) tinh khiết. Ý nghĩa duy nhất của bất kỳ thuật ngữ nào trong phép tính lambda của chúng tôi được xác định bởi giá trị trả về của nó. Ngoại lệ duy nhất là tác dụng phụ của việc không chấm dứt được thể hiện bằng thuật ngữ Omega:
-- in simple LC syntax:
--
-- (\x -> (x x)) (\x -> (x x))
omega :: Expr
omega = App (Abs "x" (App (Var "x")
(Var "x")))
(Abs "x" (App (Var "x")
(Var "x")))
Thú vị (không tinh khiết) đóng cửa
Bây giờ đối với một số nền tảng nhất định, các bao đóng được mô tả trong LC đơn giản ở trên thật nhàm chán vì không có khái niệm nào về khả năng tương tác với các biến chúng tôi đã đóng. Cụ thể, từ "đóng cửa" có xu hướng gọi mã như Javascript sau
> function mk_counter() {
var n = 0;
return function incr() {
return n += 1;
}
}
undefined
> var c = mk_counter()
undefined
> c()
1
> c()
2
> c()
3
Điều này chứng tỏ rằng chúng ta đã đóng trên n
biến trong hàm bên trong incr
và gọi incr
tương tác có ý nghĩa với biến đó. mk_counter
là thuần túy, nhưng incr
không tinh khiết (và cũng không minh bạch về mặt tham chiếu).
Điều gì khác nhau giữa hai trường hợp này?
Khái niệm "biến"
Nếu chúng ta xem xét sự thay thế và trừu tượng có nghĩa gì trong ý nghĩa LC đơn giản, chúng ta nhận thấy rằng chúng hoàn toàn đơn giản. Các biến có nghĩa đen không gì khác hơn là tra cứu môi trường ngay lập tức. Trừu tượng Lambda theo nghĩa đen không gì khác hơn là tạo ra một môi trường tăng cường để đánh giá biểu thức bên trong. Không có chỗ trong mô hình này cho loại hành vi mà chúng ta đã thấy với mk_counter
/ incr
vì không có biến thể nào được cho phép.
Đối với nhiều người, đây là trung tâm của "biến" có nghĩa là biến thể của Haiti. Tuy nhiên, các nhà ngữ nghĩa học muốn phân biệt giữa loại biến được sử dụng trong LC và loại "biến" được sử dụng trong Javascript. Để làm như vậy, họ có xu hướng gọi cái sau là "ô có thể thay đổi" hoặc "khe".
Danh pháp này theo cách sử dụng "biến" trong lịch sử lâu dài trong toán học, trong đó nó có nghĩa giống như "không xác định": biểu thức (toán học) "x + x" không cho phép x
thay đổi theo thời gian, thay vào đó nó có nghĩa là bất kể của giá trị (đơn, không đổi) x
mất.
Do đó, chúng tôi nói "vị trí" để nhấn mạnh khả năng đưa các giá trị vào một vị trí và đưa chúng ra ngoài.
Để thêm sự nhầm lẫn, trong Javascript, các "vị trí" này trông giống như các biến: chúng tôi viết
var x;
để tạo một và sau đó khi chúng ta viết
x;
nó cho biết chúng tôi đang tìm kiếm giá trị hiện được lưu trữ trong khe đó. Để làm cho điều này rõ ràng hơn, các ngôn ngữ thuần túy có xu hướng nghĩ về các vị trí như lấy tên làm tên (toán học, tính toán lambda). Trong trường hợp này, chúng tôi phải dán nhãn rõ ràng khi chúng tôi nhận hoặc đặt từ một vị trí. Ký hiệu như vậy có xu hướng trông giống như
-- create a fresh, empty slot and name it `x` in the context of the
-- expression E
let x = newSlot in E
-- look up the value stored in the named slot named `x`, return that value
get x
-- store a new value, `v`, in the slot named `x`, return the slot
put x v
Ưu điểm của ký hiệu này là giờ đây chúng ta có sự phân biệt rõ ràng giữa các biến toán học và các vị trí có thể thay đổi. Các biến có thể lấy các vị trí làm giá trị của chúng, nhưng vị trí cụ thể được đặt tên bởi một biến là không đổi trong phạm vi của nó.
Sử dụng ký hiệu này, chúng ta có thể viết lại mk_counter
ví dụ (lần này theo cú pháp giống Haskell, mặc dù ngữ nghĩa giống như Haskell đã quyết định):
mkCounter =
let x = newSlot
in (\() -> let old = get x
in get (put x (old + 1)))
Trong trường hợp này, chúng tôi đang sử dụng các quy trình thao túng vị trí có thể thay đổi này. Để thực hiện nó, chúng ta cần phải đóng không chỉ một môi trường liên tục của các tên như x
mà còn là một môi trường có thể thay đổi chứa tất cả các vị trí cần thiết. Điều này gần với khái niệm chung về "đóng cửa" mọi người rất yêu thích.
Một lần nữa, mkCounter
là rất không tinh khiết. Nó cũng rất mờ đục. Nhưng lưu ý rằng các tác dụng phụ không phát sinh từ việc bắt hoặc đóng tên mà thay vào đó là việc bắt giữ các tế bào có thể thay đổi và các hoạt động tác dụng phụ trên nó như thế nào get
và put
.
Cuối cùng, tôi nghĩ rằng đây là câu trả lời cuối cùng cho câu hỏi của bạn: độ tinh khiết không bị ảnh hưởng bởi việc bắt biến (toán học) mà thay vào đó là các hoạt động hiệu ứng phụ được thực hiện trên các vị trí có thể thay đổi được đặt tên bởi các biến đã bắt.
Chỉ có điều trong các ngôn ngữ không cố gắng gần gũi với LC hoặc không cố gắng duy trì độ tinh khiết mà hai khái niệm này thường bị lẫn lộn dẫn đến nhầm lẫn.