Efek Samping Melanggar Transparansi Referensial


11

Pemrograman Fungsional di Scala menjelaskan dampak efek samping pada melanggar transparansi referensial:

efek samping, yang menyiratkan beberapa pelanggaran transparansi referensial.

Saya telah membaca bagian dari SICP , yang membahas penggunaan “model substitusi” untuk mengevaluasi suatu program.

Karena saya secara kasar memahami model substitusi dengan transparansi referensial (RT), Anda dapat menguraikan fungsi menjadi bagian-bagiannya yang paling sederhana. Jika ekspresi adalah RT, maka Anda dapat menghapus komposisi ekspresi dan selalu mendapatkan hasil yang sama.

Namun, seperti yang dinyatakan dalam kutipan di atas, menggunakan efek samping dapat / akan memecah model substitusi.

Contoh:

val x = foo(50) + bar(10)

Jika foodan bar tidak memiliki efek samping, maka mengeksekusi salah satu fungsi akan selalu mengembalikan hasil yang sama x. Tetapi, jika mereka memiliki efek samping, mereka akan mengubah variabel yang mengganggu / melemparkan kunci pas ke dalam model substitusi.

Saya merasa nyaman dengan penjelasan ini, tetapi saya tidak sepenuhnya mengerti.

Harap perbaiki saya dan isi setiap lubang sehubungan dengan efek samping yang melanggar RT, diskusikan juga efeknya pada model substitusi.

Jawaban:


20

Mari kita mulai dengan definisi untuk transparansi referensial :

Ekspresi dikatakan transparan referensial jika dapat diganti dengan nilainya tanpa mengubah perilaku suatu program (dengan kata lain, menghasilkan program yang memiliki efek dan output yang sama pada input yang sama).

Artinya adalah bahwa (misalnya) Anda dapat mengganti 2 + 5 dengan 7 di bagian mana pun dari program ini, dan program itu tetap bekerja. Proses ini disebut substitusi. Pergantian berlaku jika, dan hanya jika, 2 + 5 dapat diganti dengan 7 tanpa mempengaruhi bagian lain dari program .

Katakanlah saya memiliki kelas yang dipanggil Baz, dengan fungsi Foodan Bardi dalamnya. Untuk kesederhanaan, kami hanya akan mengatakan itu Foodan Barkeduanya mengembalikan nilai yang dilewatkan. Jadi Foo(2) + Bar(5) == 7, seperti yang Anda harapkan. Referensi Transparansi menjamin bahwa Anda dapat mengganti ekspresi Foo(2) + Bar(5)dengan ekspresi 7di mana saja dalam program Anda, dan program tersebut akan tetap berfungsi secara identik.

Tetapi bagaimana jika Foomengembalikan nilai yang diteruskan, tetapi Barmengembalikan nilai yang diteruskan, ditambah nilai terakhir yang diberikan Foo? Itu cukup mudah dilakukan jika Anda menyimpan nilai Foodalam variabel lokal di dalam Bazkelas. Nah, jika nilai awal variabel lokal itu adalah 0, ekspresi Foo(2) + Bar(5)akan mengembalikan nilai yang diharapkan 7saat pertama kali Anda memintanya, tetapi itu akan mengembalikan 9saat kedua Anda memintanya.

Ini melanggar transparansi referensial dua arah. Pertama, Bar tidak dapat diandalkan untuk mengembalikan ekspresi yang sama setiap kali dipanggil. Kedua, efek samping telah terjadi, yaitu bahwa memanggil Foo memengaruhi nilai balik Bar. Karena Anda tidak lagi dapat menjamin bahwa Foo(2) + Bar(5)akan sama dengan 7, Anda tidak dapat lagi mengganti.

Inilah yang dimaksud Transparansi Referensial dalam praktik; fungsi transparan referensial menerima sejumlah nilai, dan mengembalikan beberapa nilai yang sesuai, tanpa memengaruhi kode lain di tempat lain dalam program, dan selalu mengembalikan output yang sama dengan input yang sama.


5
Jadi, RTmelumpuhkan Anda menonaktifkan penggunaan substitution model.Masalah besar dengan tidak dapat menggunakan substitution modeladalah kekuatan menggunakannya untuk alasan tentang suatu program?
Kevin Meredith

Benar sekali.
Robert Harvey

1
+1 jawaban yang sangat jelas dan dapat dimengerti. Terima kasih.
Racheet

2
Juga jika fungsi-fungsi itu transparan atau "murni" urutan bahwa mereka benar-benar berjalan tidak penting, kami tidak peduli jika foo () atau bar () berjalan terlebih dahulu, dan dalam beberapa kasus mereka mungkin tidak pernah mengevaluasi jika mereka tidak diperlukan
Zachary K

1
Namun keuntungan lain dari RT adalah bahwa ekspresi transparan referensial mahal dapat di-cache (karena mengevaluasi mereka sekali atau dua kali harus menghasilkan hasil yang sama persis).
dcastro

3

Bayangkan Anda sedang mencoba untuk membangun dinding dan Anda telah diberikan bermacam-macam kotak dalam berbagai ukuran dan bentuk. Anda perlu mengisi lubang berbentuk L tertentu di dinding; Anda harus mencari kotak berbentuk L atau Anda bisa mengganti dua kotak lurus dengan ukuran yang sesuai?

Dalam dunia fungsional, jawabannya adalah bahwa salah satu solusi akan berhasil. Saat membangun dunia fungsional Anda, Anda tidak perlu membuka kotak untuk melihat apa yang ada di dalamnya.

Di dunia imperatif, membangun tembok Anda berbahaya tanpa memeriksa isi setiap kotak dan membandingkannya dengan isi setiap kotak lainnya:

  • Beberapa berisi magnet yang kuat dan akan mendorong kotak magnetik lainnya keluar dari dinding jika tidak selaras.
  • Beberapa sangat panas atau dingin dan akan bereaksi buruk jika ditempatkan di ruang yang berdekatan.

Saya pikir saya akan berhenti sebelum saya menghabiskan waktu Anda dengan metafora yang lebih tidak mungkin, tapi saya harap intinya dibuat; batu bata fungsional tidak mengandung kejutan tersembunyi dan sepenuhnya dapat diprediksi. Karena Anda selalu dapat menggunakan blok yang lebih kecil dengan ukuran dan bentuk yang tepat untuk menggantikan yang lebih besar dan tidak ada perbedaan antara dua kotak dengan ukuran dan bentuk yang sama, Anda memiliki transparansi referensial. Dengan batu bata imperatif, tidaklah cukup untuk memiliki sesuatu dengan ukuran dan bentuk yang tepat - Anda harus tahu bagaimana batu bata itu dibangun. Tidak transparan referensial.

Dalam bahasa fungsional murni, yang perlu Anda lihat adalah tanda tangan fungsi untuk mengetahui fungsinya. Tentu saja, Anda mungkin ingin melihat ke dalam untuk melihat seberapa baik kinerjanya, tetapi Anda tidak harus melihatnya.

Dalam bahasa imperatif, Anda tidak pernah tahu kejutan apa yang mungkin tersembunyi di dalam.


"Dalam bahasa fungsional murni, yang perlu Anda lihat adalah tanda tangan fungsi untuk mengetahui fungsinya." - Itu umumnya tidak benar. Ya, dengan asumsi polimorfisme parametrik kita dapat menyimpulkan bahwa fungsi tipe (a, b) -> ahanya dapat fstfungsi dan bahwa fungsi tipe a -> ahanya dapat identityfungsi, tetapi Anda tidak bisa mengatakan apa-apa tentang fungsi tipe (a, a) -> a, misalnya.
Jörg W Mittag

2

Seperti yang saya kira memahami model substitusi (dengan transparansi referensial (RT)), Anda dapat menguraikan fungsi menjadi bagian-bagian yang paling sederhana. Jika ekspresi adalah RT, maka Anda dapat menghapus komposisi ekspresi dan selalu mendapatkan hasil yang sama.

Ya, intuisinya benar. Berikut adalah beberapa petunjuk untuk mendapatkan yang lebih tepat:

Seperti yang Anda katakan, setiap ekspresi RT harus memiliki single"hasil". Artinya, diberi factorial(5)ekspresi dalam program, itu harus selalu menghasilkan "hasil" yang sama. Jadi, jika seseorang factorial(5)berada dalam program dan menghasilkan 120, itu harus selalu menghasilkan 120 terlepas dari "urutan langkah" mana yang diperluas / dihitung - terlepas dari waktu .

Contoh: factorialfungsi.

def factorial(n):
    if n == 1:
        return 1
    return n * factorial(n - 1)

Ada beberapa pertimbangan dengan penjelasan ini.

Pertama-tama, perlu diingat model evaluasi yang berbeda (lihat urutan aplikatif vs normal) dapat menghasilkan "hasil" yang berbeda untuk ekspresi RT yang sama.

def first(y, z):
  return y

def second(x):
  return second(x)

first(2, second(3)) # result depends on eval. model

Dalam kode di atas, firstdan secondsecara referensi transparan, namun, ekspresi pada akhirnya menghasilkan "hasil" yang berbeda jika dievaluasi dalam urutan normal dan urutan aplikatif (di bawah yang terakhir, ekspresi tidak berhenti).

.... yang mengarah ke penggunaan "hasil" dalam tanda kutip. Karena tidak diperlukan ekspresi untuk berhenti, itu mungkin tidak menghasilkan nilai. Jadi menggunakan "hasil" agak kabur. Bisa dikatakan ekspresi RT selalu menghasilkan hal yang sama di computationsbawah model evaluasi.

Ketiga, mungkin diperlukan untuk melihat dua yang foo(50)muncul di program di lokasi yang berbeda sebagai ekspresi yang berbeda - masing-masing menghasilkan hasil mereka sendiri yang mungkin berbeda satu sama lain. Misalnya, jika bahasa memungkinkan ruang lingkup dinamis, kedua ekspresi, meskipun identik secara leksikal, berbeda. Dalam perl:

sub foo {
    my $x = shift;
    return $x + $y; # y is dynamic scope var
}

sub a {
    local $y = 10;
    return &foo(50); # expanded to 60
}

sub b {
    local $y = 20;
    return &foo(50); # expanded to 70
}

Lingkup dinamis menyesatkan karena membuatnya mudah bagi seseorang untuk berpikir xadalah satu-satunya masukan untuk foo, padahal pada kenyataannya, itu adalah xdan y. Salah satu cara untuk melihat perbedaannya adalah dengan mengubah program menjadi program yang setara tanpa ruang lingkup dinamis - yaitu, memberikan parameter secara eksplisit, jadi alih-alih mendefinisikan foo(x), kami mendefinisikan foo(x, y)dan meneruskan ysecara eksplisit dalam penelepon.

Intinya adalah, kita selalu berada di bawah functionpola pikir: diberi input tertentu untuk ekspresi, kita diberi "hasil" yang sesuai. Jika kita memberikan input yang sama, kita harus selalu mengharapkan "hasil" yang sama.

Sekarang, bagaimana dengan kode berikut?

def foo():
   global y
   y = y + 1
   return y

y = 10
foo() # yields 11
foo() # yields 12

The fooprosedur istirahat RT karena ada redefinitions. Yaitu, kita mendefinisikan ydalam satu titik, dan yang terakhir, mendefinisikan ulang hal yang sama y . Dalam contoh perl di atas, ys adalah binding yang berbeda meskipun mereka memiliki nama huruf yang sama "y". S di sini ysebenarnya sama. Itu sebabnya kami mengatakan penugasan kembali adalah operasi meta : Anda sebenarnya mengubah definisi program Anda.

Secara kasar, orang biasanya menggambarkan perbedaan sebagai berikut: di pengaturan bebas efek samping, Anda memiliki pemetaan input -> output. Dalam pengaturan "imperatif", Anda memiliki input -> ouputkonteks stateyang dapat berubah sepanjang waktu.

Sekarang, alih-alih hanya mengganti ekspresi untuk nilai yang sesuai, kita juga harus menerapkan transformasi statepada setiap operasi yang memerlukannya (dan tentu saja, ekspresi dapat berkonsultasi dengan hal yang sama stateuntuk melakukan perhitungan).

Jadi, jika dalam program bebas efek samping semua yang perlu kita ketahui untuk menghitung ekspresi adalah input individualnya, dalam program imperatif, kita perlu mengetahui input dan seluruh kondisi, untuk setiap langkah komputasi. Penalaran adalah yang pertama mengalami pukulan besar (sekarang, untuk men-debug prosedur yang bermasalah, Anda memerlukan input dan dump inti). Trik tertentu dianggap tidak praktis, seperti memoisasi. Tetapi juga, konkurensi dan paralelisme menjadi jauh lebih menantang.


1
Bagus bahwa Anda menyebutkan memoisasi. Ini dapat digunakan sebagai contoh keadaan internal yang tidak terlihat di luar: fungsi menggunakan memoisasi masih transparan secara referensi meskipun secara internal ia menggunakan keadaan dan mutasi.
Giorgio
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.