Apakah penutupan dianggap gaya fungsional yang tidak murni?

33

Apakah penutupan dianggap tidak murni dalam pemrograman fungsional?

Tampaknya seseorang umumnya dapat menghindari penutupan dengan memberikan nilai langsung ke suatu fungsi. Karena itu, apakah penutupan harus dihindari jika memungkinkan?

Jika mereka tidak murni, dan saya benar dalam menyatakan bahwa mereka dapat dihindari, mengapa begitu banyak bahasa pemrograman fungsional mendukung penutupan?

Salah satu kriteria untuk fungsi murni adalah bahwa "Fungsi selalu mengevaluasi nilai hasil yang sama dengan nilai argumen yang sama ."

Seharusnya

f: x -> x + y

f(3)tidak akan selalu memberikan hasil yang sama. f(3)tergantung pada nilai yyang bukan merupakan argumen f. Jadi fbukan fungsi murni.

Karena semua penutupan bergantung pada nilai-nilai yang bukan argumen, bagaimana mungkin penutupan apa pun menjadi murni? Ya, secara teori nilai tertutup bisa konstan tetapi tidak ada cara untuk mengetahui bahwa hanya dengan melihat kode sumber fungsi itu sendiri.

Hal ini menuntun saya ke adalah bahwa fungsi yang sama mungkin murni dalam satu situasi tetapi tidak murni dalam situasi lain. Seseorang tidak selalu dapat menentukan apakah suatu fungsi murni atau tidak dengan mempelajari kode sumbernya. Sebaliknya, seseorang mungkin harus mempertimbangkannya dalam konteks lingkungannya pada titik ketika ia dipanggil sebelum perbedaan tersebut dapat dibuat.

Apakah saya memikirkan hal ini dengan benar?

pengguna2179977
sumber
6
Saya menggunakan penutupan sepanjang waktu di Haskell, dan Haskell murni seperti yang didapatnya.
Thomas Eding
5
Dalam bahasa fungsional murni, ytidak bisa berubah, sehingga output dari f(3)akan selalu sama.
Lily Chung
4
yadalah bagian dari definisi fwalaupun itu tidak secara eksplisit ditandai sebagai input f- itu masih merupakan kasus yang fdidefinisikan dalam hal y(kita dapat menunjukkan fungsi f_y, untuk membuat ketergantungan pada yeksplisit), dan oleh karena itu perubahan ymemberikan fungsi yang berbeda . Fungsi khusus yang f_ydidefinisikan untuk suatu fungsi ysangat murni. (Misalnya, kedua fungsi f: x -> x + 3dan fungsi f: x -> x + 5yang berbeda , dan keduanya murni, meskipun kami kebetulan menggunakan huruf yang sama untuk menyatakannya.)
ShreevatsaR

Jawaban:

26

Kemurnian dapat diukur dengan dua hal:

  1. Apakah fungsi selalu mengembalikan output yang sama, diberi input yang sama; yaitu apakah itu transparan referensial?
  2. Apakah fungsi memodifikasi sesuatu di luar dirinya sendiri, yaitu apakah ia memiliki efek samping?

Jika jawaban 1 adalah ya dan jawaban 2 tidak, maka fungsinya murni. Penutupan hanya membuat fungsi tidak murni jika Anda memodifikasi variabel tertutup.

Robert Harvey
sumber
Bukankah determinisme item pertama? Atau apakah itu juga bagian dari kesucian? Saya tidak terlalu akrab dengan konsep "kemurnian" dalam konteks pemrograman.
4
@JimmyHoffa: Belum tentu. Anda dapat menempatkan output dari pengatur waktu perangkat keras dalam suatu fungsi, dan tidak ada yang di luar fungsi yang dimodifikasi.
Robert Harvey
1
@RobertHarvey Apakah ini tentang bagaimana kita mendefinisikan input ke suatu fungsi? Kutipan saya dari wikipedia berfokus pada argumen fungsi, sedangkan Anda juga menganggap variabel tertutup sebagai input.
user2179977
8
@ user2179977: kecuali mereka bisa berubah, Anda tidak boleh mempertimbangkan variabel tertutup sebagai input tambahan untuk fungsi. Sebaliknya, Anda harus menganggap penutupan itu sendiri sebagai fungsi, dan menjadi fungsi yang berbeda ketika ditutup dengan nilai yang berbeda y. Jadi misalnya kita mendefinisikan fungsi gsedemikian rupa sehingga fungsi itu g(y)sendiri x -> x + y. Kemudian gadalah fungsi integer yang mengembalikan fungsi, g(3)adalah fungsi integer yang mengembalikan integer, dan g(2)merupakan fungsi berbeda dari integer yang mengembalikan integer. Ketiga fungsi itu murni.
Steve Jessop
1
@ Arkhogg: Ya. Lihat pembaruan saya.
Robert Harvey
10

Penutupan muncul Lambda Calculus, yang merupakan bentuk paling murni dari pemrograman fungsional yang mungkin, jadi saya tidak akan menyebut mereka "tidak murni" ...

Penutupan tidak "tidak murni" karena fungsi dalam bahasa fungsional adalah warga negara kelas satu - yang berarti mereka dapat diperlakukan sebagai nilai.

Bayangkan ini (kodesemu):

foo(x) {
    let y = x + 1
    ...
}

yadalah sebuah nilai. Nilainya tergantung x, tetapi xtidak berubah sehingga ynilainya juga tidak berubah. Kita dapat memanggil fooberkali-kali dengan argumen berbeda yang akan menghasilkan ys yang berbeda , tetapi yitu semua hidup dalam cakupan yang berbeda dan bergantung pada xs yang berbeda sehingga kemurnian tetap utuh.

Sekarang mari kita ubah:

bar(x) {
    let y(z) = x + z
    ....
}

Di sini kita menggunakan closure (kita menutup x), tapi itu sama seperti in foo- calls barberbeda dengan argumen yang berbeda menciptakan nilai yang berbeda dari y(ingat - fungsi adalah nilai) yang semuanya tidak dapat diubah sehingga kemurnian tetap utuh.

Perlu diketahui juga bahwa penutupan memiliki efek yang sangat mirip dengan kari:

adder(a)(b) {
    return a + b
}
baz(x) {
    let y = adder(x)
    ...
}

baztidak benar-benar berbeda dari bar- di keduanya kita membuat nilai fungsi bernama yyang mengembalikan argumen plus itu x. Sebenarnya, dalam Kalkulus Lambda Anda menggunakan penutupan untuk membuat fungsi dengan beberapa argumen - dan itu masih tidak murni.

Idan Arye
sumber
9

Orang lain telah meliput pertanyaan umum dengan baik dalam jawaban mereka, jadi saya hanya akan melihat membersihkan kebingungan yang Anda berikan pada hasil edit Anda.

Penutupan tidak menjadi input dari fungsi, melainkan 'masuk' ke fungsi tubuh. Agar lebih konkret, fungsi mengacu pada nilai dalam lingkup luar di tubuhnya.

Anda mendapat kesan bahwa itu membuat fungsi tidak murni. Itu tidak terjadi, secara umum. Dalam pemrograman fungsional, nilai-nilai sebagian besar tidak berubah . Itu berlaku untuk nilai penutupan juga.

Katakanlah Anda memiliki kode seperti ini:

let make y =
    fun x -> x + y

Memanggil make 3dan make 4akan memberikan dua fungsi dengan penutupan lebih make's yargumen. Salah satu dari mereka akan kembali x + 3, yang lain x + 4. Namun mereka adalah dua fungsi yang berbeda, dan keduanya murni. Mereka dibuat menggunakan makefungsi yang sama , tetapi hanya itu.

Catat sebagian besar waktu beberapa paragraf kembali.

  1. Di Haskell, yang murni, Anda hanya bisa menutup nilai yang tidak berubah. Tidak ada negara yang bisa berubah untuk ditutup. Anda yakin mendapatkan fungsi murni dengan cara itu.
  2. Dalam bahasa fungsional yang tidak murni, seperti F #, Anda dapat menutup sel referensi dan tipe referensi, dan mendapatkan fungsi yang tidak murni. Anda benar bahwa Anda harus melacak ruang lingkup di mana fungsi didefinisikan untuk mengetahui apakah itu murni atau tidak. Anda dapat dengan mudah mengetahui apakah suatu nilai bisa berubah dalam bahasa-bahasa itu, sehingga tidak terlalu menjadi masalah.
  3. Dalam bahasa OOP yang mendukung penutupan, seperti C # dan JavaScript, situasinya mirip dengan bahasa fungsional yang tidak murni, tetapi melacak lingkup luar menjadi lebih rumit karena variabel dapat diubah secara default.

Perhatikan bahwa untuk 2 dan 3, bahasa-bahasa itu tidak menawarkan jaminan apa pun tentang kemurnian. Kenajisan di sana bukan milik penutupan, tetapi dari bahasa itu sendiri. Penutupan tidak banyak mengubah gambar sendiri.

scrwtp
sumber
1
Anda benar-benar dapat menutup nilai yang dapat diubah di Haskell, tetapi hal seperti itu akan dijelaskan dengan monad IO.
Daniel Gratzer
1
@ jozefg tidak, Anda menutup IO Anilai yang tidak dapat diubah , dan tipe penutupan Anda IO (B -> C)atau semacamnya. Kemurnian dipertahankan
Caleth
5

Biasanya saya akan meminta Anda untuk menjelaskan definisi "tidak murni" Anda, tetapi dalam hal ini tidak terlalu penting. Anggap Anda mengontraskannya dengan istilah yang murni fungsional , jawabannya adalah "tidak", karena tidak ada penutupan yang secara inheren bersifat destruktif. Jika bahasa Anda benar-benar berfungsi tanpa penutupan, itu masih akan berfungsi murni dengan penutupan. Jika sebaliknya yang Anda maksud "tidak berfungsi", jawabannya tetap "tidak"; Penutupan memfasilitasi pembuatan fungsi.

Tampaknya seseorang umumnya dapat menghindari penutupan dengan mengirimkan data langsung ke suatu fungsi.

Ya, tetapi kemudian fungsi Anda akan memiliki satu parameter lagi, dan itu akan mengubah tipenya. Penutupan memungkinkan Anda membuat fungsi berdasarkan variabel tanpa menambahkan parameter. Ini berguna ketika Anda memiliki, katakanlah, fungsi yang membutuhkan 2 argumen, dan Anda ingin membuat versi yang hanya membutuhkan 1 argumen.

EDIT: Sehubungan dengan edit / contoh Anda sendiri ...

Seharusnya

f: x -> x + y

f (3) tidak akan selalu memberikan hasil yang sama. f (3) tergantung pada nilai y yang bukan merupakan argumen dari f. Jadi f bukan fungsi murni.

Tergantung adalah pilihan kata yang salah di sini. Mengutip artikel Wikipedia yang sama dengan yang Anda lakukan:

Dalam pemrograman komputer, suatu fungsi dapat digambarkan sebagai fungsi murni jika kedua pernyataan ini tentang fungsi tersebut:

  1. Fungsi selalu mengevaluasi nilai hasil yang sama dengan nilai argumen yang sama. Nilai hasil fungsi tidak dapat bergantung pada informasi atau keadaan tersembunyi yang dapat berubah saat eksekusi program berlangsung atau antara berbagai eksekusi program, juga tidak dapat bergantung pada input eksternal dari perangkat I / O.
  2. Evaluasi hasil tidak menyebabkan efek samping atau keluaran yang dapat diamati secara semantik, seperti mutasi objek yang dapat berubah atau keluaran ke perangkat I / O.

Dengan asumsi ytidak berubah (yang biasanya terjadi dalam bahasa fungsional), kondisi 1 puas: untuk semua nilai x, nilai f(x)tidak berubah. Ini harus jelas dari fakta yang ytidak berbeda dari konstanta, dan x + 3murni. Juga jelas tidak ada mutasi atau I / O yang terjadi.

Doval
sumber
3

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 addEnvketika kita menambah lingkungan dengan nama baru. Fungsi ini dipanggil hanya "di dalam" dari Absistilah traksi yang ditafsirkan (istilah lambda). Lingkungan akan "mendongak" setiap kali kita mengevaluasi suatu Varistilah dan oleh karena Varitu tekad untuk apa pun yang Namedimaksud dalam Envyang ditangkap oleh Abstraksi 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 nvariabel dalam fungsi dalam incrdan memanggil incrinteraksi yang bermakna dengan variabel itu. mk_countermurni, tetapi incrjelas 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/ incrkarena 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 xbervariasi dari waktu ke waktu, itu malah dimaksudkan untuk memiliki makna terlepas dari dari nilai (tunggal, konstan) yang xdiambil.

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_countercontoh (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, mkCountersangat 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 getdan 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.

J. Abrahamson
sumber
1

Tidak, closure tidak menyebabkan fungsi menjadi tidak murni, selama nilai tertutupnya konstan (tidak diubah oleh closure atau kode lain), yang merupakan kasus umum dalam pemrograman fungsional.

Perhatikan bahwa meskipun Anda selalu dapat memberikan nilai sebagai argumen, biasanya Anda tidak dapat melakukannya tanpa kesulitan yang berarti. Misalnya (naskah kopi):

closedValue = 42
return (arg) -> console.log "#{closedValue} #{arg}"

Dengan saran Anda, Anda bisa mengembalikan:

return (arg, closedValue) -> console.log "#{closedValue} #{arg}"

Fungsi ini tidak dipanggil pada titik ini, hanya ditentukan , jadi Anda harus menemukan beberapa cara untuk melewatkan nilai yang Anda inginkan closedValueke titik di mana fungsi sebenarnya dipanggil. Paling-paling ini menciptakan banyak kopling. Paling buruk, Anda tidak mengontrol kode pada titik panggilan, sehingga secara efektif tidak mungkin.

Pustaka acara dalam bahasa yang tidak mendukung penutupan biasanya menyediakan cara lain untuk meneruskan data sewenang-wenang kembali ke panggilan balik, tetapi tidak cantik, dan menciptakan banyak kerumitan baik untuk pengelola perpustakaan dan pengguna perpustakaan.

Karl Bielefeldt
sumber