Apa yang membuat bahasa pemrograman fungsional deklaratif dan bukan Imperatif?

17

Pada banyak artikel, menggambarkan manfaat pemrograman fungsional, saya telah melihat bahasa pemrograman fungsional, seperti Haskell, ML, Scala atau Clojure, disebut sebagai "bahasa deklaratif" yang berbeda dari bahasa imperatif seperti C / C ++ / C # / Java. Pertanyaan saya adalah apa yang membuat bahasa pemrograman fungsional deklaratif dan bukan imperatif.

Penjelasan yang sering dijumpai menggambarkan perbedaan antara pemrograman Deklaratif dan Imperatif adalah bahwa dalam pemrograman Imperatif Anda memberi tahu komputer "Cara melakukan sesuatu" yang bertentangan dengan "Apa yang harus dilakukan" dalam bahasa deklaratif. Masalah yang saya miliki dengan penjelasan ini adalah bahwa Anda terus melakukan keduanya dalam semua bahasa pemrograman. Bahkan jika Anda turun ke unit level terendah Anda masih memberi tahu komputer "Apa yang harus dilakukan", Anda memberi tahu CPU untuk menambahkan dua angka, Anda tidak menginstruksikannya tentang bagaimana melakukan penambahan. Jika kita pergi ke ujung lain spektrum, bahasa fungsional murni tingkat tinggi seperti Haskell, Anda sebenarnya memberi tahu komputer cara mencapai tugas tertentu, itulah yang program Anda adalah urutan instruksi untuk mencapai tugas tertentu yang komputer tidak tahu bagaimana mencapainya sendirian. Saya mengerti bahwa bahasa-bahasa seperti Haskell, Clojure, dll. Jelas tingkat lebih tinggi dari C / C ++ / C # / Java dan menawarkan fitur-fitur seperti evaluasi malas, struktur data yang tidak dapat diubah, fungsi anonim, penjelajahan, struktur data persisten, dll. Semuanya membuat pemrograman fungsional mungkin dan efisien, tetapi saya tidak akan mengklasifikasikannya sebagai bahasa deklaratif.

Bahasa deklaratif murni bagi saya akan menjadi bahasa yang hanya terdiri dari deklarasi saja, contoh bahasa seperti itu adalah CSS (Ya saya tahu CSS secara teknis tidak akan menjadi bahasa pemrograman). CSS hanya berisi deklarasi gaya yang digunakan oleh HTML dan Javascript halaman. CSS tidak dapat melakukan hal lain selain membuat deklarasi, ia tidak dapat membuat fungsi kelas, yaitu fungsi yang menentukan gaya untuk ditampilkan berdasarkan beberapa parameter, Anda tidak dapat menjalankan skrip CSS, dll. Bagi saya itu menggambarkan bahasa deklaratif (perhatikan saya tidak mengatakan deklaratif bahasa pemrograman ).

Memperbarui:

Saya telah bermain-main dengan Prolog baru-baru ini dan bagi saya, Prolog adalah bahasa pemrograman terdekat dengan bahasa deklaratif penuh (Setidaknya menurut saya), jika itu bukan satu-satunya bahasa pemrograman deklaratif sepenuhnya. Untuk menguraikan pemrograman dalam Prolog dilakukan dengan membuat deklarasi yang menyatakan fakta (fungsi predikat yang mengembalikan true untuk input tertentu) atau aturan (fungsi predikat yang mengembalikan true untuk kondisi / pola tertentu berdasarkan input), aturan didefinisikan menggunakan teknik pencocokan pola. Untuk melakukan apa pun di prolog, Anda meminta basis pengetahuan dengan mengganti satu atau lebih input predikat dengan variabel dan prolog mencoba menemukan nilai untuk variabel yang berhasil dilakukan predikat.

Maksud saya adalah di prolog tidak ada instruksi penting, Anda pada dasarnya mengatakan (menyatakan) komputer apa yang ia ketahui dan kemudian bertanya (bertanya) tentang pengetahuan. Dalam bahasa pemrograman fungsional, Anda masih memberikan instruksi, misalnya mengambil nilai, memanggil fungsi X dan menambahkan 1 ke dalamnya, dll, bahkan jika Anda tidak secara langsung memanipulasi lokasi memori atau menulis langkah demi langkah perhitungan. Saya tidak akan mengatakan bahwa pemrograman dalam Haskell, ML, Scala atau Clojure adalah deklaratif dalam pengertian ini, meskipun saya mungkin salah. Adalah tepat, benar, deklaratif pemrograman fungsional murni dalam arti yang saya jelaskan di atas.

ALXGTV
sumber
Di mana @JimmyHoffa saat Anda membutuhkannya?
2
1. C # dan C ++ modern adalah bahasa yang sangat fungsional. 2. Pikirkan perbedaan penulisan SQL, dan Java untuk mencapai tujuan yang sama (mungkin beberapa permintaan kompleks dengan gabungan). Anda juga dapat berpikir bagaimana LINQ dalam C # berbeda dari menulis loop foreach sederhana ...
AK_
juga difirence lebih merupakan masalah gaya daripada sesuatu yang didefinisikan dengan jelas ... kecuali jika Anda menggunakan prolog ...
AK_
1
Salah satu kunci untuk mengenali perbedaan adalah ketika Anda menggunakan operator penugasan , vs. operator definisi / deklarasi - di Haskell misalnya tidak ada operator penugasan . Perilaku kedua operator ini sangat berbeda dan memaksa Anda untuk merancang kode Anda secara berbeda.
Jimmy Hoffa
1
Sayangnya bahkan tanpa operator penugasan saya dapat melakukan ini (let [x 1] (let [x (+ x 2)] (let [x (* x x)] x)))(Semoga Anda mengerti itu, Clojure). Pertanyaan orisinal saya adalah apa yang membuat ini berbeda dari int ini x = 1; x += 2; x *= x; return x;Menurut saya sebagian besar sama.
ALXGTV

Jawaban:

16

Anda tampaknya menarik garis antara menyatakan sesuatu dan menginstruksikan mesin. Tidak ada pemisahan yang keras dan cepat. Karena mesin yang diinstruksikan dalam pemrograman imperatif tidak harus berupa perangkat keras fisik, ada banyak kebebasan untuk interpretasi. Hampir semuanya dapat dilihat sebagai program eksplisit untuk mesin abstrak yang tepat. Misalnya, Anda bisa melihat CSS sebagai bahasa tingkat tinggi untuk memprogram mesin yang terutama menyelesaikan penyeleksi dan menetapkan atribut objek DOM yang dipilih.

Pertanyaannya adalah apakah perspektif seperti itu masuk akal, dan sebaliknya, seberapa dekat urutan instruksi menyerupai deklarasi hasil yang dihitung. Untuk CSS, perspektif deklaratif jelas lebih bermanfaat. Bagi C, perspektif imperatif jelas dominan. Adapun bahasa seperti Haskell, well ...

Bahasa telah ditentukan, semantik konkret. Artinya, tentu saja orang dapat mengartikan program sebagai rantai operasi. Bahkan tidak perlu terlalu banyak usaha untuk memilih operasi primitif sehingga mereka memetakan dengan baik ke perangkat keras komoditas (inilah yang dilakukan mesin STG dan model lainnya).

Namun, cara program Haskell ditulis, mereka sering dapat secara masuk akal dibaca sebagai deskripsi dari hasil yang akan dihitung. Ambil, misalnya, program untuk menghitung jumlah faktorial N pertama:

sum_of_fac n = sum [product [1..i] | i <- ..n]

Anda dapat menghapus ini dan membacanya sebagai urutan operasi STG, tetapi jauh lebih alami untuk membacanya sebagai deskripsi hasil (yang, saya pikir, definisi yang lebih berguna dari pemrograman deklaratif daripada "apa yang harus dihitung"): Hasilnya adalah jumlah produk [1..i]untuk semua i= 0, ..., n. Dan itu jauh lebih deklaratif daripada hampir semua program atau fungsi C.

Mathias Bynens
sumber
1
Alasan saya mengajukan pertanyaan ini adalah bahwa banyak orang mencoba untuk mempromosikan pemrograman fungsional karena beberapa paradigma baru yang benar-benar terpisah dari paradigma C / C ++ / C # / Java atau bahkan Python ketika pada kenyataannya pemrograman fungsional hanyalah bentuk tingkat yang lebih tinggi dari pemrograman imperatif.
ALXGTV
Untuk menjelaskan apa yang saya maksud di sini, Anda mendefinisikan sum_of_fac tetapi sum_of_fac tidak berguna, kecuali jika itu yang dilakukan oleh aplikasi hole Anda, Anda perlu menggunakan hasilnya untuk menghitung beberapa hasil lain yang akhirnya dibangun untuk mencapai tugas tertentu, yang tugas yang program Anda dirancang untuk dipecahkan.
ALXGTV
6
@ALXGTV Pemrograman fungsional adalah paradigma yang sangat berbeda, bahkan jika Anda bersikeras membacanya sebagai keharusan (ini adalah klasifikasi yang sangat luas, ada lebih banyak paradigma pemrograman daripada itu). Dan kode lain yang dibuat berdasarkan kode ini juga dapat dibaca secara deklaratif: Misalnya map (sum_of_fac . read) (lines fileContent),. Tentu saja pada titik tertentu I / O ikut bermain, tetapi seperti yang saya katakan, ini sebuah rangkaian. Bagian dari program Haskell lebih penting, tentu saja, sama seperti C memiliki sintaks ekspresi agak deklaratif ( x + y, tidak load x; load y; add;)
1
@ALXGTV Intuisi Anda benar: pemrograman deklaratif "hanya" memberikan tingkat abstraksi yang lebih tinggi atas rincian penting tertentu. Saya berpendapat ini masalah besar!
Andres F.
1
@ Brendan Saya tidak berpikir pemrograman fungsional adalah himpunan bagian dari pemrograman imperatif (juga tidak dapat diubahnya sifat wajib dari FP). Dapat dikatakan bahwa dalam kasus FP murni yang diketik secara statis, ini adalah superset dari pemrograman imperatif di mana efeknya eksplisit dalam jenisnya. Anda tahu pernyataan dari mulut ke mulut bahwa Haskell adalah "bahasa imperatif terbaik di dunia";)
Andres F.
21

Unit dasar dari program imperatif adalah pernyataan . Pernyataan dieksekusi karena efek sampingnya. Mereka mengubah keadaan yang mereka terima. Urutan pernyataan adalah urutan perintah, yang menunjukkan lakukan ini lalu lakukan itu. Pemrogram menentukan urutan yang tepat untuk melakukan perhitungan. Inilah yang dimaksud orang dengan memberi tahu komputer bagaimana cara melakukannya.

Unit dasar dari program deklaratif adalah ekspresi . Ekspresi tidak memiliki efek samping. Mereka menentukan hubungan antara input dan output, menciptakan output baru dan terpisah dari input mereka, daripada mengubah keadaan input mereka. Urutan ekspresi tidak ada artinya tanpa beberapa ekspresi mengandung menentukan hubungan di antara mereka. Pemrogram menentukan hubungan antara data, dan program menyimpulkan perintah untuk melakukan perhitungan dari hubungan tersebut. Inilah yang dimaksud orang dengan memberi tahu komputer apa yang harus dilakukan.

Bahasa imperatif memiliki ekspresi, tetapi cara utama mereka untuk menyelesaikan sesuatu adalah pernyataan. Demikian juga, bahasa deklaratif memiliki beberapa ekspresi dengan semantik yang mirip dengan pernyataan, seperti urutan monad di dalam donotasi Haskell , tetapi pada intinya, mereka adalah satu ekspresi besar. Hal ini memungkinkan pemula menulis kode yang tampak sangat imperatif, tetapi kekuatan sebenarnya dari bahasa tersebut datang ketika Anda lolos dari paradigma itu.

Karl Bielefeldt
sumber
1
Ini akan menjadi jawaban yang sempurna jika berisi contoh. Contoh terbaik yang saya tahu untuk menggambarkan deklaratif vs penting adalah Linq vs foreach.
Robert Harvey
7

Karakteristik mendefinisikan nyata yang memisahkan deklaratif dari pemrograman imperatif adalah dalam gaya deklaratif Anda tidak memberikan instruksi berurutan; di level yang lebih rendah ya CPU beroperasi dengan cara ini, tetapi itu menjadi perhatian kompiler.

Anda menyarankan CSS adalah "bahasa deklaratif", saya tidak akan menyebutnya bahasa sama sekali. Ini adalah format struktur data, seperti JSON, XML, CSV, atau INI, hanya format untuk mendefinisikan data yang diketahui oleh seorang penafsir.

Beberapa efek samping yang menarik terjadi ketika Anda mengambil operator penugasan dari bahasa, dan ini adalah penyebab sebenarnya untuk kehilangan semua instruksi imperatif step1-step2-step3 dalam bahasa deklaratif.

Apa yang Anda lakukan dengan operator penugasan dalam suatu fungsi? Anda membuat langkah menengah. Itulah intinya, operator penugasan digunakan untuk mengubah data secara bertahap. Segera setelah Anda tidak lagi dapat mengubah data, Anda kehilangan semua langkah itu dan berakhir dengan:

Setiap fungsi hanya memiliki satu pernyataan, pernyataan ini menjadi deklarasi tunggal

Sekarang ada banyak cara untuk membuat pernyataan tunggal terlihat seperti banyak pernyataan, tapi itu hanya tipuan, misalnya: 1 + 3 + (2*4) + 8 - (7 / (8*3))ini jelas pernyataan tunggal, tetapi jika Anda menuliskannya sebagai ...

1 +
3 +
(
  2*4
) +
8 - 
(
  7 / (8*3)
)

Ini dapat mengambil tampilan urutan operasi yang dapat lebih mudah bagi otak untuk mengenali dekomposisi yang diinginkan penulis bermaksud. Saya sering melakukan ini dalam C # dengan kode seperti ..

people
  .Where(person => person.MiddleName != null)
  .Select(person => person.MiddleName)
  .Distinct();

Ini adalah pernyataan tunggal, tetapi menunjukkan penampilan yang terurai menjadi banyak - perhatikan tidak ada tugas.

Perhatikan juga, dalam kedua metode di atas - cara kode sebenarnya dieksekusi tidak dalam urutan Anda segera membacanya; karena kode ini mengatakan apa yang harus dilakukan, tetapi tidak menentukan caranya . Perhatikan aritmatika sederhana di atas jelas tidak akan dieksekusi dalam urutan yang dituliskan dari kiri ke kanan, dan dalam contoh C # di atas ketiga metode tersebut sebenarnya semua masuk kembali dan tidak dieksekusi sampai selesai secara berurutan, kompilator sebenarnya menghasilkan kode yang akan melakukan apa pernyataan itu ingin , tetapi belum tentu bagaimana Anda menganggap.

Saya percaya membiasakan diri dengan pendekatan deklaratif-tanpa-langkah-langkah ini adalah bagian yang paling sulit dari semuanya; dan mengapa Haskell sangat rumit karena hanya sedikit bahasa yang benar-benar melarangnya seperti yang dilakukan Haskell. Anda harus mulai melakukan beberapa senam yang menarik untuk menyelesaikan beberapa hal yang biasanya Anda lakukan dengan bantuan variabel perantara, seperti penjumlahan.

Dalam bahasa deklaratif, jika Anda ingin variabel perantara melakukan sesuatu dengan - itu berarti meneruskannya sebagai parameter ke fungsi yang melakukan hal itu. Inilah sebabnya mengapa rekursi menjadi sangat penting.

sum xs = (head xs) + sum (tail xs)

Anda tidak dapat membuat resultSumvariabel dan menambahkannya ketika Anda mengulanginya xs, Anda harus mengambil nilai pertama, dan menambahkannya ke jumlah penjumlahan dari segala sesuatu yang lain - dan untuk mengakses semua yang lain Anda harus meneruskan xske suatu fungsi tailkarena Anda tidak bisa hanya membuat variabel untuk xs dan pop off yang akan menjadi pendekatan yang sangat penting. (Ya saya tahu Anda bisa menggunakan destrukturisasi tetapi contoh ini dimaksudkan sebagai ilustrasi)

Jimmy Hoffa
sumber
Cara saya memahaminya, dari jawaban Anda, adalah bahwa dalam pemrograman fungsional murni, seluruh program terdiri dari banyak fungsi ekspresi tunggal, sedangkan dalam fungsi pemrograman imperatif (prosedur) berisi lebih dari satu ekspresi dengan urutan operasi yang ditentukan
ALXGTV
1 + 3 + (2 * 4) + 8 - (7 / (8 * 3)) sebenarnya adalah beberapa pernyataan / ekspresi, tentu saja itu tergantung pada apa yang Anda lihat sebagai satu ekspresi. Singkatnya pemrograman fungsional pada dasarnya adalah pemrograman tanpa titik koma.
ALXGTV
1
@ALXGTV perbedaan antara deklarasi itu dan gaya imperatif adalah bahwa jika ekspresi matematis itu pernyataan, akan sangat penting bahwa mereka dieksekusi sebagai ditulis. Namun karena itu bukan urutan pernyataan imperatif melainkan ekspresi yang mendefinisikan apa yang Anda inginkan, karena itu tidak mengatakan bagaimana , itu tidak penting bahwa itu dieksekusi sebagai ditulis. Oleh karena itu kompilasi dapat mengurangi ekspresi atau bahkan memoize itu sebagai literal. Bahasa imperatif juga melakukan ini, tetapi hanya ketika Anda memasukkannya ke dalam satu pernyataan; sebuah deklarasi.
Jimmy Hoffa
@ALXGTV Deklarasi mendefinisikan hubungan, hubungan dapat ditempa; jika x = 1 + 2kemudian x = 3dan x = 4 - 1. Pernyataan berurutan menentukan instruksi sehingga ketika x = (y = 1; z = 2 + y; return z;)Anda tidak lagi memiliki sesuatu yang dapat ditempa, sesuatu yang dapat dikompilasi oleh kompiler dengan berbagai cara - lebih penting kompiler melakukan apa yang Anda perintahkan di sana, karena kompiler tidak dapat mengetahui semua efek samping dari instruksi Anda sehingga bisa ' t mengubahnya.
Jimmy Hoffa
Anda memiliki poin yang adil.
ALXGTV
6

Aku tahu aku terlambat ke pesta, tapi aku punya pencerahan hari lain jadi begini ...

Saya pikir komentar tentang fungsional, tidak berubah dan tidak ada efek samping meleset ketika menjelaskan perbedaan antara deklaratif vs imperatif atau menjelaskan apa arti pemrograman deklaratif. Juga, seperti yang Anda sebutkan dalam pertanyaan Anda, keseluruhan "apa yang harus dilakukan" vs "bagaimana melakukannya" terlalu kabur dan benar-benar tidak menjelaskan keduanya.

Mari kita ambil kode sederhana a = b + csebagai dasar dan lihat pernyataan dalam beberapa bahasa berbeda untuk mendapatkan ide:

Ketika kita menulis a = b + cdalam bahasa imperatif, seperti C, kita menempatkan saat nilai b + cke variabel adan tidak lebih. Kami tidak membuat pernyataan mendasar tentang apa aitu. Sebaliknya, kami hanya menjalankan langkah dalam suatu proses.

Ketika kita menulis a = b + cdalam bahasa deklaratif, seperti Microsoft Excel, (ya, Excel adalah bahasa pemrograman dan mungkin yang paling deklaratif dari semuanya,) kami menyatakan hubungan antara a, bdan csedemikian rupa sehingga selalu amerupakan jumlah yang merupakan jumlah dari dua yang lainnya. Itu bukan langkah dalam suatu proses, itu adalah invarian, jaminan, pernyataan kebenaran.

Bahasa fungsional juga deklaratif, tetapi hampir tidak sengaja. Dalam Haskel misalnya a = b + cjuga menegaskan hubungan yang invarian, tetapi hanya karena bdan ctidak dapat diubah.

Jadi ya, ketika objek tidak berubah dan fungsi bebas efek samping, kode menjadi deklaratif (meskipun terlihat identik dengan kode imperatif), tetapi bukan itu intinya. Juga tidak menghindari tugas. Inti dari kode deklaratif adalah membuat pernyataan mendasar tentang hubungan.

Daniel T.
sumber
0

Anda benar bahwa tidak ada perbedaan yang jelas antara memberi tahu komputer apa yang harus dilakukan dan bagaimana melakukannya.

Namun, di satu sisi spektrum Anda hampir secara eksklusif berpikir tentang cara memanipulasi memori. Yaitu, untuk menyelesaikan masalah, Anda mempresentasikannya ke komputer dalam bentuk seperti "atur lokasi memori ini ke x, kemudian atur lokasi memori itu ke y, lompat ke lokasi memori z ..." dan kemudian entah bagaimana akhirnya Anda memiliki hasil di beberapa lokasi memori lain.

Dalam bahasa yang dikelola seperti Java, C # dan sebagainya, Anda tidak memiliki akses langsung ke memori perangkat keras lagi. Programmer imperatif sekarang memusatkan perhatian pada variasi statis, referensi atau bidang instance kelas, yang semuanya dalam beberapa derajat absraksi untuk lokasi memori.

Dalam bahasa languga seperti Haskell, OTOH, memori benar-benar hilang. Ini tidak terjadi di

f a b = let y = sum [a..b] in y*y

harus ada dua sel memori yang memegang argumen a dan b dan satu lagi yang memegang hasil antara y. Yang pasti, backend kompiler dapat memancarkan kode final yang bekerja dengan cara ini (dan dalam beberapa hal ia harus melakukannya selama arsitektur target adalah mesin v. Neumann).

Tetapi intinya adalah kita tidak perlu menginternalisasi arsitektur v. Neumann untuk memahami fungsi di atas. Kita juga tidak memerlukan komputer kontemporer untuk menjalankannya. Misalnya, akan mudah untuk menerjemahkan program dalam bahasa FP murni ke mesin hipotetis yang berfungsi di dasar kalkulus SKI. Sekarang coba hal yang sama dengan program C!

Bahasa deklaratif murni bagi saya akan menjadi bahasa yang hanya terdiri dari deklarasi saja

Ini tidak cukup kuat, IMHO. Bahkan program C hanyalah urutan deklarasi. Saya merasa kita harus memenuhi syarat deklarasi lebih lanjut. Sebagai contoh, apakah mereka memberi tahu kita apa sesuatu itu (deklaratif) atau apa yang dilakukannya (imperatif).

Ingo
sumber
Saya tidak menyatakan bahwa pemrograman fungsional tidak berbeda dengan pemrograman dalam C, pada kenyataannya saya mengatakan bahwa bahasa seperti Haskell berada pada tingkat yang jauh lebih tinggi daripada C dan menawarkan abstraksi yang jauh lebih besar.
ALXGTV