Saya sedang membaca sebuah artikel tentang pemrograman fungsional di mana penulis menyatakan
(take 25 (squares-of (integers)))
Perhatikan bahwa ia tidak memiliki variabel. Memang, ia tidak lebih dari tiga fungsi dan satu konstanta. Cobalah menulis kuadrat bilangan bulat di Jawa tanpa menggunakan variabel. Oh, mungkin ada cara untuk melakukannya, tetapi tentu saja itu tidak alami, dan itu tidak akan sebagus program saya di atas.
Apakah mungkin untuk mencapai ini di Jawa? Andaikan Anda diminta untuk mencetak kuadrat dari 15 bilangan bulat pertama, dapatkah Anda menulis untuk atau sementara loop tanpa menggunakan variabel?
Pemberitahuan mod
Pertanyaan ini bukan kontes kode golf. Kami mencari jawaban yang menjelaskan konsep-konsep yang terlibat (idealnya tanpa mengulangi jawaban sebelumnya), dan bukan hanya untuk sepotong kode lagi.
sumber
squaresOf(integers()).take(25)
(menulis fungsi-fungsi itu dibiarkan sebagai latihan untuk pembaca; kesulitannya terletak pada set yang tak terbatas untukintegers()
, tapi itu masalah untuk Jawa karena evaluasi yang bersemangat, tidak ada hubungannya dengan variabel)Jawaban:
Apakah mungkin untuk menerapkan contoh seperti itu di Jawa tanpa menggunakan pembaruan yang merusak? Iya. Namun, seperti yang disebutkan oleh @Thiton dan artikelnya, itu akan menjadi jelek (tergantung selera seseorang). Salah satu caranya adalah menggunakan rekursi; inilah contoh Haskell yang melakukan hal serupa:
Catatan 1) kurangnya mutasi, 2) penggunaan rekursi, dan 3) kurangnya loop. Poin terakhir sangat penting - bahasa fungsional tidak memerlukan konstruksi perulangan yang dibangun ke dalam bahasa, karena rekursi dapat digunakan untuk sebagian besar (semua?) Kasus di mana loop digunakan di Jawa. Berikut adalah serangkaian makalah terkenal yang menunjukkan bagaimana panggilan fungsi yang sangat ekspresif dapat terjadi.
Saya menemukan artikel yang tidak memuaskan dan ingin membuat beberapa poin tambahan:
Artikel itu adalah penjelasan yang sangat buruk dan membingungkan tentang pemrograman fungsional dan manfaatnya. Saya akan sangat merekomendasikan sumber lain untuk belajar tentang pemrograman fungsional.
Bagian yang paling membingungkan tentang artikel ini adalah bahwa ia tidak menyebutkan bahwa ada dua kegunaan untuk pernyataan penugasan di Jawa (dan sebagian besar bahasa utama lainnya):
mengikat nilai ke nama:
final int MAX_SIZE = 100;
pembaruan destruktif:
int a = 3; a += 1; a++;
Pemrograman fungsional menghindari yang kedua, tetapi merangkul yang pertama (contoh:
let
-ekspresi, parameter fungsi,define
ition tingkat atas ) . Ini adalah hal yang sangat penting untuk memahami, karena jika tidak artikel tersebut sepertinya konyol dan mungkin meninggalkan Anda bertanya-tanya, apatake
,squares-of
danintegers
jika tidak variabel?Selain itu, contohnya tidak ada artinya. Ini tidak menunjukkan implementasi
take
,squares-of
atauintegers
. Sejauh yang kita tahu, mereka diimplementasikan menggunakan variabel yang bisa berubah. Seperti yang dikatakan @Martin, Anda dapat dengan sepele menulis contoh ini di Java.Sekali lagi, saya akan merekomendasikan menghindari artikel ini dan orang lain menyukainya jika Anda benar-benar ingin belajar tentang pemrograman fungsional. Tampaknya ditulis lebih dengan tujuan mengejutkan dan menyinggung daripada mengajarkan konsep dan dasar-dasar. Sebaliknya, mengapa tidak memeriksa salah satu kertas favorit saya sepanjang masa , oleh John Hughes. Hughes mencoba untuk mengatasi beberapa masalah yang sama dengan artikel tersebut (walaupun Hughes tidak berbicara tentang konkurensi / paralelisasi); inilah sebuah teaser:
sumber
take 25 (map (^2) [1..])
sebagai contoh?[1..]
? Itu fitur keren bawaan Haskell, tetapi tidak menggambarkan konsep di balik pembuatan daftar semacam itu. Saya yakin contoh dariEnum
kelas (yang membutuhkan sintaks) juga membantu, tetapi terlalu malas untuk menemukannya. Jadiunfoldr
,. :)Kamu tidak akan Variabel adalah inti dari pemrograman imperatif, dan jika Anda mencoba memprogram secara imperatif tanpa menggunakan variabel, Anda hanya akan membuat semua orang kesakitan. Dalam paradigma pemrograman yang berbeda, gaya berbeda, dan konsep yang berbeda membentuk dasar Anda. Variabel di Jawa, bila digunakan dengan baik dengan cakupan kecil, tidak jahat. Meminta program Java tanpa variabel seperti meminta program Haskell tanpa fungsi, jadi Anda tidak memintanya, dan Anda tidak membiarkan diri Anda tertipu melihat pemrograman imperatif lebih rendah karena menggunakan variabel.
Jadi, cara Java adalah:
dan jangan biarkan diri Anda dibodohi untuk menulisnya dengan cara yang lebih rumit karena kebencian terhadap variabel.
sumber
Yang paling sederhana yang bisa saya lakukan dengan rekursi adalah fungsi dengan satu parameter. Ini bukan Java-esque, tetapi ini berfungsi:
sumber
Dalam contoh fungsional Anda, kami tidak melihat bagaimana
squares-of
dantake
fungsi diimplementasikan. Saya bukan ahli Java, tapi saya cukup yakin kita bisa menulis fungsi-fungsi itu untuk mengaktifkan pernyataan seperti ini ...yang tidak begitu jauh berbeda.
sumber
squares-of
bukan nama yang valid di Java (squares_of
is though). Tetapi sebaliknya, poin bagus yang menunjukkan bahwa contoh artikel itu buruk.integer
malas menghasilkan bilangan bulat, dantake
fungsinya mengambil 25squared-of
angkainteger
. Singkatnya, Anda harus memilikiinteger
fungsi untuk menghasilkan bilangan bulat malas hingga tak terbatas.(integer)
fungsi - suatu fungsi masih sesuatu yang memetakan argumen ke suatu nilai. Ternyata itu(integer)
bukan fungsi, tetapi hanya nilai. Seseorang bahkan dapat melangkah lebih jauh dengan mengatakan bahwa ituinteger
adalah variabel yang terikat pada untaian angka yang tak terbatas.Di Jawa Anda bisa melakukan ini (terutama bagian daftar tak terbatas) dengan iterator. Dalam contoh kode berikut, jumlah yang dipasok ke
Take
konstruktor dapat tinggi secara sewenang-wenang.Atau dengan metode pabrik rantai:
Di mana
SquaresOf
,Take
danIntegers
luaskanNumbers
sumber
while (test.hasNext()) System.out.println(test.next())
akan menjadi no-no di FP; iterator secara inheren bisa berubah.Versi pendek:
Agar gaya penugasan tunggal berfungsi dengan baik di Jawa, Anda perlu (1) semacam infrastruktur ramah-abadi, dan (2) dukungan tingkat kompiler atau runtime untuk eliminasi panggilan ekor.
Kami dapat menulis banyak infrastruktur, dan kami dapat mengatur berbagai hal untuk menghindari pengisian tumpukan. Tetapi selama setiap panggilan membutuhkan frame stack, akan ada batasan berapa banyak rekursi yang dapat Anda lakukan. Simpan iterables Anda kecil dan / atau malas, dan Anda seharusnya tidak memiliki masalah besar. Setidaknya sebagian besar masalah yang akan Anda hadapi tidak membutuhkan pengembalian sejuta hasil sekaligus. :)
Perhatikan juga, karena program harus benar-benar memengaruhi perubahan yang terlihat agar layak untuk dijalankan, Anda tidak dapat membuat semuanya berubah. Anda dapat, bagaimanapun, menjaga sebagian besar barang-barang Anda sendiri tidak berubah, menggunakan subset kecil dari perumpamaan penting (stream, misalnya) hanya pada titik-titik kunci tertentu di mana alternatif akan terlalu berat.
Versi panjang:
Sederhananya, program Java tidak dapat sepenuhnya menghindari variabel jika ingin melakukan sesuatu yang layak dilakukan. Anda dapat menampungnya , dan dengan demikian membatasi mutabilitas hingga tingkat yang sangat besar, tetapi desain bahasa dan API yang sangat - bersama dengan kebutuhan untuk akhirnya mengubah sistem yang mendasarinya - membuat kekekalan total menjadi tidak mungkin.
Java dirancang sejak awal sebagai bahasa imperatif , berorientasi objek .
while (true)
danfor (;;)
! - sangat tergantung pada variabel di suatu tempat yang berubah dari iterasi ke iterasi.Hasil akhir dari keputusan desain tersebut adalah bahwa tanpa variabel yang dapat berubah, Java tidak memiliki cara untuk mengubah keadaan apa pun - bahkan sesuatu yang sederhana seperti mencetak "Halo dunia!" ke layar melibatkan aliran output, yang melibatkan menempel byte dalam buffer yang bisa berubah .
Jadi, untuk semua tujuan praktis, kita dibatasi untuk membuang variabel dari kode kita sendiri . OK, kita bisa melakukan itu. Hampir. Pada dasarnya yang kita butuhkan adalah mengganti hampir semua iterasi dengan rekursi, dan semua mutasi dengan panggilan rekursif mengembalikan nilai yang diubah. seperti itu ...
Pada dasarnya, kita membangun daftar tertaut, di mana setiap node adalah daftar itu sendiri. Setiap daftar memiliki "kepala" (nilai saat ini) dan "ekor" (sublist yang tersisa). Sebagian besar bahasa fungsional melakukan sesuatu yang mirip dengan ini, karena itu sangat bisa diterima untuk kekekalan efisien. Operasi "berikutnya" hanya mengembalikan buntut, yang biasanya diteruskan ke tingkat berikutnya dalam setumpuk panggilan rekursif.
Sekarang, ini adalah versi yang sangat disederhanakan dari barang ini. Tapi itu cukup bagus untuk menunjukkan masalah serius dengan pendekatan ini di Jawa. Pertimbangkan kode ini:
Meskipun kami hanya membutuhkan 25 int untuk hasilnya,
squares_of
tidak tahu itu. Ini akan mengembalikan kuadrat dari setiap angka diintegers
. Rekursi 20 juta level menyebabkan masalah yang cukup besar di Jawa.Lihat, bahasa fungsional Anda biasanya melakukan keanehan seperti ini, memiliki fitur yang disebut "eliminasi panggilan ekor". Apa itu artinya, ketika kompiler melihat tindakan terakhir kode adalah untuk memanggil dirinya sendiri (dan mengembalikan hasilnya jika fungsi tidak batal), ia menggunakan bingkai stack panggilan saat ini alih-alih mengatur yang baru dan melakukan "lompatan" sebagai gantinya dari "panggilan" (sehingga ruang stack yang digunakan tetap konstan). Singkatnya, ia berjalan sekitar 90% dari jalan menuju mengubah rekursi ekor menjadi iterasi. Itu bisa berurusan dengan milyaran int tanpa meluap tumpukan. (Ini pada akhirnya masih kehabisan memori, tetapi merakit daftar milyaran int akan tetap mengacaukan Anda dengan memori pada sistem 32-bit.)
Java tidak melakukan itu, dalam banyak kasus. (Itu tergantung pada kompiler dan runtime, tetapi implementasi Oracle tidak melakukannya.) Setiap panggilan ke fungsi rekursif memakan memori stack. Gunakan terlalu banyak, dan Anda mendapatkan stack overflow. Meluap tumpukan semua tetapi menjamin kematian program. Jadi kita harus memastikan untuk tidak melakukannya.
Satu semi-solusi ... evaluasi malas. Kami masih memiliki batasan tumpukan, tetapi mereka dapat dikaitkan dengan faktor-faktor yang memiliki kendali lebih besar. Kami tidak harus menghitung satu juta ints hanya untuk mengembalikan 25. :)
Jadi mari kita bangun beberapa infrastruktur evaluasi malas. (Kode ini sudah diuji beberapa waktu lalu, tapi saya sudah memodifikasinya sejak itu; baca idenya, bukan kesalahan sintaks. :))
(Perlu diingat bahwa jika ini benar-benar layak di Jawa, kode setidaknya seperti di atas sudah akan menjadi bagian dari API.)
Sekarang, dengan infrastruktur yang tersedia, agak sepele untuk menulis kode yang tidak perlu variabel yang dapat diubah dan setidaknya stabil untuk jumlah input yang lebih kecil.
Ini sebagian besar berfungsi, tetapi masih cenderung untuk menumpuk kelebihan. Coba
take
2 miliar int dan lakukan beberapa tindakan pada mereka. : P Pada akhirnya akan mengeluarkan pengecualian, setidaknya hingga 64+ GB RAM menjadi standar. Masalahnya adalah, jumlah memori program yang dicadangkan untuk tumpukannya tidak terlalu besar. Biasanya antara 1 dan 8 MiB. (Anda dapat meminta yang lebih besar, tetapi tidak masalah seberapa banyak yang Anda minta - Anda menelepontake(1000000000, someInfiniteSequence)
, Anda akan mendapatkan pengecualian.) Untungnya, dengan evaluasi yang malas, titik lemah ada di area yang dapat kita kontrol lebih baik . Kita hanya harus berhati-hati tentang berapa banyak kitatake()
.Masih akan ada banyak masalah yang ditingkatkan, karena penggunaan tumpukan kami meningkat secara linear. Setiap panggilan menangani satu elemen dan meneruskan sisanya ke panggilan lain. Sekarang saya berpikir tentang hal itu, ada satu trik yang bisa kita tarik yang mungkin memberi kita sedikit lebih banyak ruang kepala: mengubah rantai panggilan menjadi pohon panggilan. Pertimbangkan sesuatu yang lebih seperti ini:
workWith
pada dasarnya memecah pekerjaan menjadi dua bagian, dan menetapkan masing-masing setengah untuk panggilan lain untuk dirinya sendiri. Karena setiap panggilan mengurangi ukuran daftar kerja menjadi setengah daripada satu, ini harus skala logaritma daripada linear.Masalahnya adalah, fungsi ini membutuhkan input - dan dengan daftar yang ditautkan, mendapatkan panjang memerlukan melintasi seluruh daftar. Namun itu mudah dipecahkan; hanya tidak peduli berapa banyak entri ada. :) Kode di atas akan berfungsi dengan sesuatu seperti
Integer.MAX_VALUE
hitungan, karena null tetap menghentikan pemrosesan. Hitungannya sebagian besar ada sehingga kami memiliki alas yang kuat. Jika Anda mengantisipasi memiliki lebih dariInteger.MAX_VALUE
entri dalam daftar, maka Anda dapat memeriksaworkWith
nilai kembali - itu harus nol pada akhirnya. Kalau tidak, kambuh.Perlu diingat, ini menyentuh elemen sebanyak yang Anda katakan. Itu tidak malas; ia melakukan hal itu dengan segera. Anda hanya ingin melakukannya untuk tindakan - yaitu, hal-hal yang tujuan utamanya adalah untuk diterapkan sendiri ke setiap elemen dalam daftar. Seperti yang saya pikirkan sekarang, menurut saya urutannya akan jauh lebih rumit jika dijaga tetap linier; seharusnya tidak menjadi masalah, karena sekuens tidak menyebut diri mereka sendiri - mereka hanya membuat objek yang memanggil mereka lagi.
sumber
Saya sebelumnya telah mencoba membuat interpreter untuk bahasa mirip-lisp di Jawa, (beberapa tahun yang lalu dan semua kode hilang seperti di CVS di sourceforge), dan Java util iterators agak verbose untuk pemrograman fungsional pada daftar.
Berikut ini sesuatu yang didasarkan pada antarmuka urutan, yang hanya memiliki dua operasi yang Anda butuhkan untuk mendapatkan nilai saat ini dan mendapatkan urutan mulai dari elemen berikutnya. Ini dinamai kepala dan ekor setelah fungsi dalam skema.
Sangat penting untuk menggunakan sesuatu seperti
Seq
atauIterator
antarmuka karena artinya daftar dibuat dengan malas. TheIterator
antarmuka tidak dapat menjadi objek abadi, sehingga kurang cocok untuk pemrograman fungsional - jika Anda tidak bisa mengatakan jika nilai Anda masuk ke fungsi telah diubah olehnya, Anda kehilangan salah satu keuntungan utama dari pemrograman fungsional.Jelas
integers
harus menjadi daftar semua bilangan bulat, jadi saya mulai dari nol dan secara berurutan mengembalikan yang positif dan negatif.Ada dua versi kotak - satu membuat urutan kustom, yang lain menggunakan
map
yang mengambil 'fungsi' - Java 7 tidak memiliki lambda jadi saya menggunakan antarmuka - dan menerapkannya ke setiap elemen dalam urutan pada gilirannya.Inti dari
square ( int x )
fungsi ini hanya untuk menghapus kebutuhan untuk memanggilhead()
dua kali - biasanya saya akan melakukan ini dengan meletakkan nilai ke dalam variabel akhir, tetapi menambahkan fungsi ini berarti tidak ada variabel dalam program, hanya parameter fungsi.Verbositas Java untuk pemrograman semacam ini membuat saya menulis versi kedua penerjemah saya di C99.
sumber
Secara teori Anda dapat mengimplementasikan hampir semua hal di Jawa menggunakan hanya rekursi dan tidak ada variabel yang bisa berubah.
Dalam praktek:
Bahasa Jawa tidak dirancang untuk ini. Banyak konstruksi dirancang untuk mutasi, dan sulit digunakan tanpa mutasi. (Misalnya, Anda tidak dapat menginisialisasi array Java panjang variabel tanpa mutasi.)
Ditto untuk perpustakaan. Dan jika Anda membatasi diri Anda pada kelas perpustakaan yang tidak menggunakan mutasi di balik sampulnya, itu bahkan lebih sulit. (Anda bahkan tidak dapat menggunakan String ... lihat bagaimana
hashcode
penerapannya.)Implementasi Mainstream Java tidak mendukung pengoptimalan tail-call. Itu berarti bahwa versi algoritma rekursif cenderung menumpuk ruang "lapar". Dan karena tumpukan thread Java tidak tumbuh, Anda perlu mengalokasikan tumpukan besar ... atau risiko
StackOverflowError
.Gabungkan ketiga hal ini, dan Java sebenarnya bukan pilihan yang layak untuk menulis program yang berguna (yaitu non-sepele) tanpa variabel yang dapat diubah.
(Tapi hei, tidak apa-apa. Ada bahasa pemrograman lain yang tersedia untuk JVM, beberapa di antaranya mendukung pemrograman fungsional.)
sumber
Saat kita mencari contoh konsep, saya katakan mari kita mengesampingkan Jawa dan mencari pengaturan yang berbeda namun akrab di mana untuk menemukan versi konsep yang akrab. Pipa UNIX agak mirip dengan chaining fungsi malas.
Di Linux ini artinya, beri saya byte yang masing-masing terdiri dari bit palsu dan bukan benar, sampai saya kehilangan nafsu makan; ubah setiap byte tersebut menjadi karakter baris baru; nomor setiap baris yang dibuat; dan menghasilkan kuadrat dari angka itu. Selain itu saya memiliki selera untuk 25 baris dan tidak lebih.
Saya mengklaim bahwa seorang programmer tidak akan sakit untuk menulis pipa Linux dengan cara itu. Linux scripting shell yang relatif normal.
Saya mengklaim bahwa seorang programmer akan sakit menyarankan untuk mencoba menulis hal yang sama dengan cara yang sama di Jawa. Alasannya adalah pemeliharaan perangkat lunak sebagai faktor utama dalam biaya seumur hidup proyek perangkat lunak. Kami tidak ingin membingungkan programmer berikutnya dengan menghadirkan apa yang seolah-olah sebuah program Java tetapi sebenarnya ditulis dengan efek dalam bahasa sekali pakai khusus dengan menduplikasi fungsionalitas rumit yang sudah ada di platform Java.
Di sisi lain, saya mengklaim bahwa programmer selanjutnya bisa lebih menerima jika beberapa paket "Java" kami sebenarnya adalah paket Java Virtual Machine yang ditulis dalam salah satu bahasa fungsional atau objek / fungsional seperti Clojure dan Scala. Ini dirancang untuk dikodekan dengan fungsi chaining bersama dan dipanggil dari Jawa dengan cara biasa panggilan metode Java.
Kemudian lagi, itu masih bisa menjadi ide yang baik bagi seorang programmer Java untuk mengambil inspirasi dari pemrograman fungsional, di beberapa tempat.
Baru-baru ini teknik favorit saya adalah menggunakan variabel return yang tidak diinisialisasi dan keluar tunggal sehingga, seperti yang dilakukan oleh beberapa kompiler bahasa fungsional, Java memeriksa bahwa apa pun yang terjadi dalam tubuh fungsi, saya selalu menyediakan satu dan hanya satu nilai pengembalian. Contoh:sumber
int result = -n; if (n < 1) { result = 0 } return result;
dikompilasi dengan baik dan kompiler tidak tahu apakah Anda bermaksud membuatnya setara dengan fungsi dalam contoh saya. Mungkin contoh itu terlalu sederhana untuk membuat teknik terlihat membantu, tetapi dalam fungsi dengan banyak cabang saya merasa senang untuk menjelaskan bahwa hasilnya diberikan tepat sekali terlepas dari jalur apa yang diikuti.if (n < 1) return 0; else return -n;
, Anda berakhir tanpa masalah ... dan lebih sederhana lagi. :) Tampak bagi saya bahwa dalam kasus itu, aturan "satu pengembalian" sebenarnya membantu menyebabkan masalah tidak mengetahui kapan nilai pengembalian Anda ditetapkan; jika tidak, Anda bisa mengembalikannya dan Java akan lebih bisa menentukan kapan jalur lain mungkin tidak mengembalikan nilai, karena Anda tidak lagi menceraikan perhitungan nilai dari pengembalian yang sebenarnya.if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;
,.Cara termudah untuk mengetahui hal itu adalah memberi makan yang berikut ke kompiler Frege , dan lihat kode java yang dihasilkan:
sumber