Apa itu transparansi referensial?

38

Saya telah melihatnya dalam paradigma imperatif

f (x) + f (x)

mungkin tidak sama dengan:

2 * f (x)

Tetapi dalam paradigma fungsional itu harus sama. Saya telah mencoba untuk mengimplementasikan kedua kasus dalam Python dan Skema , tetapi bagi saya mereka terlihat cukup mudah sama.

Apa yang akan menjadi contoh yang bisa menunjukkan perbedaan dengan fungsi yang diberikan?

Asgard
sumber
7
Anda dapat, dan sering melakukan, menulis fungsi transparan referensial dengan python. Perbedaannya adalah bahasa tidak memberlakukannya.
Karl Bielefeldt
5
di C dan sama: f(x++)+f(x++)mungkin tidak sama dengan 2*f(x++)(di C itu terutama indah ketika hal-hal seperti itu tersembunyi di dalam makro - apakah saya mematahkan hidung saya pada itu? Anda bertaruh)
Agak
Dalam pemahaman saya, contoh @ gnat adalah mengapa bahasa yang berorientasi fungsional seperti R menggunakan pass-by-reference dan secara eksplisit menghindari fungsi yang mengubah argumen mereka. Di R, setidaknya, sebenarnya bisa sulit untuk membatasi pembatasan ini (setidaknya, dengan cara yang stabil, portabel) tanpa menggali ke dalam sistem lingkungan dan ruang nama dan ruang pencarian yang rumit.
shadowtalker
4
@ssdecontrol: Sebenarnya, ketika Anda memiliki transparansi referensial, pass-by-value dan pass-by-reference selalu menghasilkan hasil yang sama persis, jadi tidak masalah mana yang digunakan bahasa. Bahasa fungsional sering dispesifikasikan dengan sesuatu yang mirip dengan nilai demi nilai untuk kejelasan semantik, tetapi implementasinya sering menggunakan pass-by-referensi untuk kinerja (atau bahkan keduanya, tergantung mana yang lebih cepat untuk konteks yang diberikan).
Jörg W Mittag
4
@gnat: Khususnya, f(x++)+f(x++)bisa benar-benar apa saja, karena itu memanggil perilaku yang tidak terdefinisi. Tapi itu tidak benar-benar terkait dengan transparansi referensial - yang tidak akan membantu untuk panggilan ini, itu 'tidak terdefinisi' untuk fungsi transparan referensial seperti sin(x++)+sin(x++)juga. Bisa jadi 42, bisa memformat hard drive Anda, bisa membuat setan terbang keluar dari hidung pengguna ...
Christopher Creutzig

Jawaban:

62

Transparansi referensial, disebut fungsi, menunjukkan bahwa Anda dapat menentukan hasil penerapan fungsi itu hanya dengan melihat nilai argumennya. Anda dapat menulis fungsi yang secara transparan transparan dalam bahasa pemrograman apa pun, misalnya Python, Skema, Pascal, C.

Di sisi lain, dalam sebagian besar bahasa Anda juga dapat menulis fungsi yang tidak transparan secara referensial. Misalnya, fungsi Python ini:

counter = 0

def foo(x):
  global counter

  counter += 1
  return x + counter

sebenarnya tidak transparan, pada kenyataannya panggilan

foo(x) + foo(x)

dan

2 * foo(x)

akan menghasilkan nilai yang berbeda, untuk argumen apa pun x. Alasan untuk ini adalah bahwa fungsi menggunakan dan memodifikasi variabel global, oleh karena itu hasil setiap doa tergantung pada keadaan yang berubah ini, dan tidak hanya pada argumen fungsi.

Haskell, sebuah bahasa yang sepenuhnya fungsional, secara ketat memisahkan evaluasi ekspresi di mana fungsi - fungsi murni diterapkan dan yang selalu transparan secara referensi, dari pelaksanaan tindakan (pemrosesan nilai-nilai khusus), yang tidak transparan secara referensial, yaitu melaksanakan tindakan yang sama dapat dilakukan setiap kali hasil berbeda.

Jadi, untuk fungsi Haskell

f :: Int -> Int

dan bilangan bulat apa pun x, selalu benar itu

2 * (f x) == (f x) + (f x)

Contoh tindakan adalah hasil dari fungsi perpustakaan getLine:

getLine :: IO String

Sebagai hasil dari evaluasi ekspresi, fungsi ini (sebenarnya konstan) pertama-tama menghasilkan nilai tipe murni IO String. Nilai dari tipe ini adalah nilai seperti yang lain: Anda bisa menyebarkannya, meletakkannya di struktur data, menyusunnya menggunakan fungsi khusus, dan sebagainya. Misalnya, Anda dapat membuat daftar tindakan seperti:

[getLine, getLine] :: [IO String]

Tindakan khusus karena Anda dapat memberitahu runtime Haskell untuk menjalankannya dengan menulis:

main = <some action>

Dalam hal ini, ketika program Haskell Anda dimulai, runtime berjalan melalui tindakan terikat maindan mengeksekusinya , mungkin menghasilkan efek samping. Oleh karena itu, eksekusi tindakan tidak transparan secara referensial karena mengeksekusi aksi yang sama dua kali dapat menghasilkan hasil yang berbeda tergantung pada apa yang didapat runtime sebagai input.

Berkat sistem tipe Haskell, sebuah aksi tidak pernah dapat digunakan dalam konteks di mana tipe lain diharapkan, dan sebaliknya. Jadi, jika Anda ingin menemukan panjang string, Anda dapat menggunakan lengthfungsi:

length "Hello"

akan kembali 5. Tetapi jika Anda ingin menemukan panjang string dibaca dari terminal, Anda tidak dapat menulis

length (getLine)

karena Anda mendapatkan kesalahan tipe: lengthmengharapkan input dari daftar jenis (dan sebuah String, memang, daftar) tetapi getLinemerupakan nilai tipe IO String(suatu tindakan). Dengan cara ini, sistem tipe memastikan bahwa nilai aksi seperti getLine(yang pelaksanaannya dilakukan di luar bahasa inti dan yang mungkin transparan non-referensial) tidak dapat disembunyikan di dalam nilai tipe non-tindakan jenis Int.

EDIT

Untuk menjawab pertanyaan yang ada, berikut ini adalah program Haskell kecil yang membaca garis dari konsol dan mencetak panjangnya.

main :: IO () -- The main program is an action of type IO ()
main = do
          line <- getLine
          putStrLn (show (length line))

Tindakan utama terdiri dari dua subaksi yang dieksekusi berurutan:

  1. getlinedari jenis IO String,
  2. yang kedua dibangun dengan mengevaluasi fungsi putStrLntipe String -> IO ()pada argumennya.

Lebih tepatnya, aksi kedua dibangun oleh

  1. mengikat linenilai yang dibaca oleh tindakan pertama,
  2. mengevaluasi fungsi-fungsi murni length(menghitung panjang sebagai integer) dan kemudian show(mengubah integer menjadi sebuah string),
  3. membangun tindakan dengan menerapkan fungsi putStrLnpada hasil show.

Pada titik ini, tindakan kedua dapat dijalankan. Jika Anda mengetik "Halo", itu akan mencetak "5".

Perhatikan bahwa jika Anda mendapatkan nilai dari suatu tindakan menggunakan <-notasi, Anda hanya dapat menggunakan nilai itu di dalam tindakan lain, misalnya Anda tidak bisa menulis:

main = do
          line <- getLine
          show (length line) -- Error:
                             -- Expected type: IO ()
                             --   Actual type: String

karena show (length line)bertipe Stringsedangkan notasi mensyaratkan bahwa suatu tindakan ( getLinebertipe IO String) diikuti oleh tindakan lain (misalnya putStrLn (show (length line))bertipe IO ()).

EDIT 2

Definisi Jörg W Mittag tentang transparansi referensial lebih umum daripada definisi saya (saya telah meningkatkan jawabannya). Saya telah menggunakan definisi terbatas karena contoh dalam pertanyaan ini berfokus pada nilai balik fungsi dan saya ingin menggambarkan aspek ini. Namun, RT secara umum merujuk pada makna keseluruhan program, termasuk perubahan keadaan global dan interaksi dengan lingkungan (IO) yang disebabkan oleh evaluasi ekspresi. Jadi, untuk definisi umum yang benar, Anda harus merujuk ke jawaban itu.

Giorgio
sumber
10
Dapatkah pemberi saran menyarankan bagaimana saya dapat meningkatkan jawaban ini?
Giorgio
Jadi bagaimana kita bisa mendapatkan panjang string dibaca dari terminal di Haskell?
sbichenko
2
Ini sangat luar biasa, tetapi demi kelengkapan, bukan sistem tipe Haskell yang memastikan aksi dan fungsi murni tidak tercampur; itu adalah fakta bahwa bahasa tidak menyediakan fungsi tidak murni yang dapat Anda panggil secara langsung. Anda sebenarnya dapat mengimplementasikan IOtipe Haskell dengan cukup mudah dalam bahasa apa pun dengan lambdas dan generik, tetapi karena siapa pun dapat menelepon printlnlangsung, implementasi IOtidak menjamin kemurnian; itu hanya sebuah konvensi.
Doval
Maksud saya, (1) semua fungsi adalah murni (tentu saja, mereka murni karena bahasa tidak memberikan yang tidak murni, meskipun sejauh yang saya tahu ada beberapa mekanisme untuk mem-bypass itu), dan (2) fungsi murni dan tindakan tidak murni memiliki tipe yang berbeda, sehingga tidak dapat dicampur. BTW, apa yang Anda maksud dengan menelepon langsung ?
Giorgio
6
Maksud Anda tentang getLinetidak transparan referensial salah. Anda menampilkan getLineseolah-olah mengevaluasi atau mengurangi beberapa String, String tertentu yang tergantung pada input pengguna. Ini salah. IO Stringtidak mengandung String lagi Maybe String. IO Stringadalah resep untuk mungkin, mungkin mendapatkan String dan, sebagai ungkapan, itu murni seperti yang lainnya di Haskell.
LuxuryMode
25
def f(x): return x()

from random import random
f(random) + f(random) == 2*f(random)
# => False

Namun, bukan itu yang dimaksud Transparansi Referensial. RT berarti bahwa Anda dapat mengganti ekspresi apa pun dalam program dengan hasil mengevaluasi ekspresi itu (atau sebaliknya) tanpa mengubah arti program.

Ambil, misalnya, program berikut:

def f(): return 2

print(f() + f())
print(2)

Program ini transparan secara referensial. Saya bisa mengganti satu atau kedua kejadian f()dengan 2dan masih akan bekerja sama:

def f(): return 2

print(2 + f())
print(2)

atau

def f(): return 2

print(f() + 2)
print(2)

atau

def f(): return 2

print(2 + 2)
print(f())

semua akan berperilaku sama.

Sebenarnya, saya curang. Saya harus bisa mengganti panggilan printdengan nilai kembalinya (yang tidak ada nilai sama sekali) tanpa mengubah arti program. Namun, jelas, jika saya hanya menghapus dua printpernyataan, makna program akan berubah: sebelumnya, ia mencetak sesuatu ke layar, setelah itu tidak. I / O tidak transparan secara referensi.

Aturan praktis yang sederhana adalah: jika Anda dapat mengganti setiap ekspresi, sub-ekspresi atau panggilan subrutin dengan nilai balik dari ekspresi, sub-ekspresi atau panggilan subrutin di mana saja dalam program, tanpa program mengubah artinya, maka Anda memiliki referensi transparansi. Dan apa artinya ini, secara praktis adalah bahwa Anda tidak dapat memiliki I / O, tidak dapat memiliki keadaan yang dapat berubah, tidak dapat memiliki efek samping. Dalam setiap ekspresi, nilai ekspresi harus semata-mata bergantung pada nilai-nilai bagian penyusun ekspresi. Dan dalam setiap panggilan subrutin, nilai balik harus sepenuhnya bergantung pada argumen.

Jörg W Mittag
sumber
4
"tidak dapat memiliki status yang dapat berubah": Ya, Anda dapat memilikinya jika disembunyikan dan tidak memengaruhi perilaku yang dapat diamati dari kode Anda. Pikirkan misalnya tentang memoisasi.
Giorgio
4
@Iorgio: Ini mungkin subjektif, tapi saya berpendapat bahwa hasil cache tidak benar-benar "keadaan bisa berubah" jika mereka disembunyikan dan tidak memiliki efek yang dapat diamati. Kekekalan selalu merupakan abstraksi yang diterapkan di atas perangkat keras yang bisa berubah; sering itu disediakan oleh bahasa (memberikan abstraksi "nilai" bahkan jika nilai dapat bergerak antara register dan lokasi memori selama eksekusi, dan dapat menghilang begitu diketahui tidak akan pernah digunakan lagi), tetapi tidak kurang valid saat itu disediakan oleh perpustakaan atau yang lainnya. (Dengan asumsi itu diimplementasikan dengan benar, tentu saja.)
ruakh
1
+1 Saya sangat suka printcontohnya. Mungkin salah satu cara untuk melihat ini, adalah apa yang dicetak di layar adalah bagian dari "nilai pengembalian". Jika Anda dapat mengganti printdengan nilai pengembalian fungsinya dan penulisan yang setara di terminal, contohnya berfungsi.
Pierre Arlaud
1
@Giorgio Penggunaan ruang / waktu tidak dapat dianggap sebagai efek samping untuk keperluan transparansi referensial. Itu akan membuat 4dan 2 + 2tidak dapat dipertukarkan karena mereka memiliki waktu berjalan yang berbeda, dan seluruh titik transparansi referensial adalah bahwa Anda dapat mengganti ekspresi dengan apa pun yang dievaluasi. Pertimbangan penting adalah keamanan benang.
Doval
1
@overexchange: Transparansi Referensial berarti Anda dapat mengganti setiap subekspresi dengan nilainya tanpa mengubah arti program. listOfSequence.append(n)kembali None, sehingga Anda harus dapat mengganti setiap panggilan listOfSequence.append(n)dengan Nonetanpa mengubah arti program Anda. Bisakah kamu melakukan itu? Jika tidak, maka tidak transparan referensial.
Jörg W Mittag
1

Sebagian dari jawaban ini diambil langsung dari tutorial yang belum selesai tentang pemrograman fungsional , yang dihosting di akun GitHub saya:

Suatu fungsi dikatakan transparan transparan jika diberi parameter input yang sama, selalu menghasilkan output yang sama (nilai balik). Jika seseorang mencari raison d'être untuk pemrograman fungsional murni, transparansi referensial adalah kandidat yang baik. Ketika bernalar dengan rumus-rumus dalam aljabar, aritmatika, dan logika, sifat ini - yang juga disebut substitusi sama dengan sama - adalah sangat penting sehingga biasanya diterima begitu saja ...

Pertimbangkan contoh sederhana:

x = 42

Dalam bahasa fungsional murni, sisi kiri dan kanan tanda sama dengan dapat disubstitusikan untuk satu sama lain. Artinya, tidak seperti dalam bahasa seperti C, notasi di atas benar-benar menegaskan kesetaraan. Konsekuensi dari ini adalah bahwa kita dapat alasan tentang kode program seperti persamaan matematika.

Dari Haskell wiki :

Komputasi murni menghasilkan nilai yang sama setiap kali dipanggil. Properti ini disebut transparansi referensial dan memungkinkan untuk melakukan penalaran yang sama pada kode ...

Untuk kontras ini, jenis operasi yang dilakukan oleh bahasa mirip C kadang-kadang disebut sebagai tugas destruktif .

Istilah murni sering digunakan untuk menggambarkan properti ekspresi, yang relevan dengan diskusi ini. Agar suatu fungsi dianggap murni,

  • itu tidak diperbolehkan untuk menunjukkan efek samping, dan
  • itu harus transparan referensial.

Menurut metafora kotak hitam, yang ditemukan di banyak buku teks matematika, fungsi internal sepenuhnya tertutup dari dunia luar. Efek samping adalah ketika suatu fungsi atau ekspresi melanggar prinsip ini - yaitu, prosedur diizinkan untuk berkomunikasi dengan beberapa cara dengan unit program lain (misalnya untuk berbagi dan bertukar informasi).

Singkatnya, transparansi referensial adalah suatu keharusan bagi fungsi untuk berperilaku seperti benar , fungsi matematika juga dalam semantik bahasa pemrograman.

yesthisisuser
sumber
ini tampaknya terbuka dengan salinan kata demi kata yang diambil dari sini : "Suatu fungsi dikatakan transparan secara referensi jika diberi parameter input yang sama, selalu menghasilkan output yang sama ..." Stack Exchange memiliki aturan untuk plagiarisme , apakah Anda tahu tentang ini? "Plagiarisme adalah tindakan tanpa jiwa untuk menyalin potongan-potongan karya orang lain, menampar namamu di atasnya dan menyerahkan dirimu sebagai penulis asli ..."
agas
3
Saya menulis halaman itu.
yesthisisuser
jika ini masalahnya, pertimbangkan untuk membuatnya tidak terlihat sebagai plagiarisme - karena pembaca tidak memiliki cara untuk mengatakannya. Apakah Anda tahu bagaimana melakukan ini di SE? 1) Anda merujuk sumber aslinya, seperti "Seperti (saya punya) tertulis [here](link to source)..." diikuti oleh 2) format kutipan yang tepat (gunakan tanda kutip, atau lebih baik lagi, > simbol untuk itu). Juga tidak ada salahnya jika selain memberikan panduan umum, alamat menjawab pertanyaan konkret yang ditanyakan, dalam hal ini tentang f(x)+f(x)/ 2*f(x), lihat Bagaimana Menjawab - jika tidak, Anda mungkin hanya mengiklankan halaman Anda
Agak
1
Secara teoritis, saya mengerti jawaban ini. Tapi, praktis mengikuti aturan ini, saya perlu mengembalikan daftar urutan hailstone dalam program ini . Bagaimana saya melakukan ini?
pertukaran berlebihan