Sangat cepat: substitusi adalah "transparan referensial" jika "mengganti yang suka dengan yang suka" dan fungsi yang "murni" jika semua efeknya terkandung dalam nilai kembalinya. Keduanya dapat dibuat tepat, tetapi penting untuk dicatat bahwa mereka tidak identik atau bahkan tidak satu sama lain.
Sekarang mari kita bicara tentang penutupan.
"Penutupan" yang membosankan (kebanyakan murni)
Penutupan terjadi karena ketika kita mengevaluasi istilah lambda kita menafsirkan variabel (terikat) sebagai pencarian lingkungan. Jadi, ketika kita mengembalikan istilah lambda sebagai hasil dari evaluasi, variabel-variabel di dalamnya akan "menutup" nilai-nilai yang mereka ambil ketika didefinisikan.
Dalam kalkulus lambda biasa, ini adalah hal yang sepele dan seluruh gagasan menghilang begitu saja. Untuk menunjukkan itu, berikut adalah juru bahasa kalkulus lambda yang relatif ringan:
-- 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)
Bagian penting yang perlu diperhatikan adalah addEnv
ketika kita menambah lingkungan dengan nama baru. Fungsi ini dipanggil hanya "di dalam" dari Abs
istilah traksi yang ditafsirkan (istilah lambda). Lingkungan akan "mendongak" setiap kali kita mengevaluasi suatu Var
istilah dan oleh karena Var
itu tekad untuk apa pun yang Name
dimaksud dalam Env
yang ditangkap oleh Abs
traksi yang mengandung Var
.
Sekarang, sekali lagi, dalam istilah LC biasa ini membosankan. Ini berarti bahwa variabel terikat hanya konstanta sejauh ada yang peduli. Mereka dievaluasi secara langsung dan segera sebagai nilai-nilai yang mereka tunjukkan dalam lingkungan sebagai leksikal mencakup hingga titik itu.
Ini juga (hampir) murni. Satu-satunya makna istilah apa pun dalam kalkulus lambda kami ditentukan oleh nilai pengembaliannya. Satu-satunya pengecualian adalah efek samping dari non-terminasi yang diwujudkan oleh istilah 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")))
Penutupan (tidak murni) yang menarik
Sekarang untuk latar belakang tertentu penutupan yang dijelaskan dalam LC sederhana di atas membosankan karena tidak ada gagasan untuk dapat berinteraksi dengan variabel yang telah kami tutup. Secara khusus, kata "closure" cenderung memohon kode seperti Javascript berikut
> function mk_counter() {
var n = 0;
return function incr() {
return n += 1;
}
}
undefined
> var c = mk_counter()
undefined
> c()
1
> c()
2
> c()
3
Ini menunjukkan bahwa kita telah menutup n
variabel dalam fungsi dalam incr
dan memanggil incr
interaksi yang bermakna dengan variabel itu. mk_counter
murni, tetapi incr
jelas tidak murni (dan tidak transparan secara referensial).
Apa yang membedakan kedua contoh ini?
Pengertian "variabel"
Jika kita melihat apa arti substitusi dan abstraksi dalam arti LC sederhana kita perhatikan bahwa mereka jelas-jelas polos. Variabel secara harfiah tidak lebih dari pencarian lingkungan langsung. Abstraksi Lambda secara harfiah tidak lebih dari menciptakan lingkungan yang diperluas untuk mengevaluasi ekspresi batin. Tidak ada ruang dalam model ini untuk jenis perilaku yang kita lihat mk_counter
/ incr
karena tidak ada variasi yang diizinkan.
Bagi banyak orang inilah inti makna "variabel" — variasi. Namun, semanticists suka membedakan antara jenis variabel yang digunakan dalam LC dan jenis "variabel" yang digunakan dalam Javascript. Untuk melakukannya, mereka cenderung menyebut yang terakhir sebagai "sel yang bisa berubah-ubah" atau "slot".
Nomenklatur ini mengikuti penggunaan historis yang panjang dari "variabel" dalam matematika di mana itu berarti sesuatu yang lebih seperti "tidak diketahui": ekspresi (matematika) "x + x" tidak memungkinkan untuk x
bervariasi dari waktu ke waktu, itu malah dimaksudkan untuk memiliki makna terlepas dari dari nilai (tunggal, konstan) yang x
diambil.
Jadi, kami mengatakan "slot" untuk menekankan kemampuan untuk menempatkan nilai ke dalam slot dan membawanya keluar.
Untuk menambah kebingungan lebih lanjut, dalam Javascript "slot" ini terlihat sama dengan variabel: kita menulis
var x;
untuk membuat satu dan kemudian ketika kita menulis
x;
itu menunjukkan kita mencari nilai yang saat ini disimpan di slot itu. Untuk membuat ini lebih jelas, bahasa murni cenderung menganggap slot sebagai mengambil nama sebagai (matematika, lambda kalkulus) nama. Dalam hal ini kita harus secara eksplisit memberi label ketika kita mendapatkan atau memasukkannya dari slot. Notasi seperti itu cenderung terlihat seperti
-- 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
Keuntungan dari notasi ini adalah bahwa kita sekarang memiliki perbedaan tegas antara variabel matematika dan slot yang bisa berubah. Variabel dapat mengambil slot sebagai nilainya, tetapi slot tertentu yang dinamai oleh variabel konstan sepanjang cakupannya.
Dengan menggunakan notasi ini, kita dapat menulis ulang mk_counter
contoh (kali ini dalam sintaksis mirip Haskell, meskipun semantik tidak seperti Haskell):
mkCounter =
let x = newSlot
in (\() -> let old = get x
in get (put x (old + 1)))
Dalam hal ini kami menggunakan prosedur yang memanipulasi slot yang dapat berubah ini. Untuk mengimplementasikannya, kita harus menutup tidak hanya lingkungan nama yang konstan sepertix
tetapi juga lingkungan yang bisa berubah yang berisi semua slot yang diperlukan. Ini lebih dekat dengan gagasan umum tentang "penutupan" yang sangat dicintai orang.
Sekali lagi, mkCounter
sangat tidak murni. Itu juga sangat referensial buram. Tetapi perhatikan bahwa efek samping tidak muncul dari penangkapan atau penutupan nama melainkan penangkapan dari sel yang bisa berubah dan operasi efek samping yang disukai get
dan put
.
Pada akhirnya, saya pikir ini adalah jawaban terakhir untuk pertanyaan Anda: kemurnian tidak dipengaruhi oleh penangkapan variabel (matematis) melainkan oleh operasi efek samping yang dilakukan pada slot yang dapat diubah yang dinamai oleh variabel yang ditangkap.
Hanya saja dalam bahasa-bahasa yang tidak berusaha dekat dengan LC atau tidak berusaha mempertahankan kemurnian, kedua konsep ini begitu sering digabungkan sehingga menimbulkan kebingungan.