Katakanlah, saya memiliki logika di bawah ini. Bagaimana cara menulis itu dalam Pemrograman Fungsional?
public int doSomeCalc(int[] array)
{
int answer = 0;
if(array!=null)
{
for(int e: array)
{
answer += e;
if(answer == 10) break;
if(answer == 150) answer += 100;
}
}
return answer;
}
Contoh-contoh di sebagian besar blog, artikel ... Saya melihat hanya menjelaskan kasus sederhana dari satu fungsi matematika lurus mengatakan 'Jumlah'. Tapi, saya punya logika yang mirip dengan yang tertulis di atas di Jawa dan ingin memigrasikannya ke kode fungsional di Clojure. Jika kami tidak dapat melakukan hal di atas dalam FP, maka jenis promosi untuk FP tidak menyatakan ini secara eksplisit.
Saya tahu bahwa kode di atas benar-benar penting. Itu tidak ditulis dengan pemikiran untuk bermigrasi ke FP di masa depan.
break
danreturn answer
dapat diganti denganreturn
di dalam loop. Dalam FP Anda dapat menerapkan pengembalian awal ini menggunakan kelanjutan, lihat mis. En.wikipedia.org/wiki/ContinuationtakeWhile
.Jawaban:
Persamaan terdekat dengan pengulangan array dalam sebagian besar bahasa fungsional adalah
fold
fungsi, yaitu fungsi yang memanggil fungsi yang ditentukan pengguna untuk setiap nilai array, meneruskan nilai akumulasi di sepanjang rantai. Dalam banyak bahasa fungsional,fold
ditambah dengan berbagai fungsi tambahan yang menyediakan fitur tambahan, termasuk opsi untuk berhenti lebih awal ketika beberapa kondisi muncul. Dalam bahasa-bahasa malas (misalnya Haskell), berhenti lebih awal dapat dicapai hanya dengan tidak mengevaluasi lebih jauh di sepanjang daftar, yang akan menyebabkan nilai-nilai tambahan tidak pernah dihasilkan. Karenanya, menerjemahkan contoh Anda ke Haskell, saya akan menuliskannya sebagai:Memecah baris ini demi baris jika Anda tidak terbiasa dengan sintaksis Haskell, ini bekerja seperti ini:
Menentukan jenis fungsi, menerima daftar int dan mengembalikan satu int.
Bagian utama dari fungsi: argumen yang diberikan
values
, returnfoldr1
dipanggil dengan argumencombine
(yang akan kita definisikan di bawah) danvalues
.foldr1
adalah varian dari primitif lipatan yang dimulai dengan akumulator yang disetel ke nilai pertama daftar (maka1
dalam nama fungsi), kemudian menggabungkannya menggunakan fungsi yang ditentukan pengguna dari kiri ke kanan (yang biasanya disebut lipatan kanan , makar
dalam nama fungsi). Jadifoldr1 f [1,2,3]
sama denganf 1 (f 2 3)
(atauf(1,f(2,3))
dalam sintaksis mirip C yang lebih konvensional).Menentukan
combine
fungsi lokal: ia menerima dua argumen,v1
danv2
. Kapanv1
10, itu hanya kembaliv1
. Dalam hal ini, v2 tidak pernah dievaluasi , sehingga loop berhenti di sini.Atau, ketika v1 adalah 150, tambahkan 100 tambahan untuk itu, dan tambahkan v2.
Dan, jika tidak satu pun dari kondisi itu benar, tambahkan saja v1 ke v2.
Sekarang, solusi ini agak spesifik untuk Haskell, karena fakta bahwa lipatan kanan berakhir jika fungsi menggabungkan tidak mengevaluasi argumen kedua disebabkan oleh strategi evaluasi malas Haskell. Saya tidak tahu Clojure, tetapi saya percaya itu menggunakan evaluasi yang ketat, jadi saya berharap itu memiliki
fold
fungsi di perpustakaan standar yang mencakup dukungan khusus untuk penghentian awal. Ini sering disebutfoldWhile
,foldUntil
atau serupa.Melihat sekilas pada dokumentasi perpustakaan Clojure menunjukkan bahwa itu sedikit berbeda dari kebanyakan bahasa fungsional dalam penamaan, dan itu
fold
bukan yang Anda cari (ini adalah mekanisme yang lebih maju yang bertujuan mengaktifkan komputasi paralel) tetapireduce
lebih langsung setara. Pengakhiran dini terjadi jikareduced
fungsi tersebut disebut dalam fungsi kombinasi Anda. Saya tidak 100% yakin saya mengerti sintaksnya, tetapi saya curiga apa yang Anda cari adalah sesuatu seperti ini:NB: kedua terjemahan, Haskell dan Clojure, tidak tepat untuk kode khusus ini; tetapi mereka menyampaikan intinya secara umum - lihat diskusi dalam komentar di bawah untuk masalah spesifik dengan contoh-contoh ini.
sumber
v1 v2
- nama membingungkan:v1
adalah "nilai dari array", tetapiv2
merupakan hasil akumulasi. dan terjemahan Anda adalah salah, saya percaya, OP lingkaran keluar ketika akumulasi (dari kiri) nilai hit 10, tidak beberapa elemen dalam array. Sama dengan kenaikan 100. Jika menggunakan lipatan di sini, gunakan lipatan kiri dengan keluar awal, beberapa variasi difoldlWhile
sini .(= v1 150)
menggunakan nilai sebelumv2
(alias.e
) Dijumlahkan untuk itu.Breaking this down line by line in case you're not familiar with Haskell's syntax
-- Kamu adalah pahlawanku. Haskell adalah misteri bagiku.Anda dapat dengan mudah mengubahnya menjadi rekursi. Dan itu memiliki panggilan rekursif dioptimalkan ekor bagus.
Kodesemu:
sumber
GOTO
. (Tidak terlalu buruk, tapi masih canggung.) Setara dengan loop, seperti kata Jules, adalah lipatan yang cocok.GOTO
. Bagaimanapun, ketika Anda mengkompilasi rekursi ekor, itu sebagian besar hanya bermuara padawhile (true)
loop dengan fungsi tubuh di mana pengembalian awal hanyalah sebuahbreak
pernyataan. Lipatan, sementara Anda benar tentang hal itu menjadi loop, sebenarnya lebih dibatasi daripada konstruksi looping umum; ini lebih mirip untuk setiap loopGOTO
adalah karena Anda perlu melakukan pembukuan fiddly argumen apa dalam keadaan apa yang diteruskan ke panggilan rekursif, untuk memastikan itu benar-benar berperilaku sebagaimana dimaksud. Itu tidak perlu pada tingkat yang sama dalam loop imperatif yang ditulis dengan baik (di mana cukup jelas apa variabel stateful dan bagaimana mereka berubah dalam setiap iterasi), atau dalam rekursi naif (di mana biasanya tidak banyak dilakukan dengan argumen, dan sebaliknya, hasil dirakit dengan cara yang cukup intuitif). ...Saya benar-benar menyukai jawaban Jules , tetapi saya juga ingin menunjukkan sesuatu yang sering orang lewatkan tentang pemrograman fungsional yang malas, yaitu bahwa segala sesuatu tidak harus "di dalam lingkaran." Sebagai contoh:
Anda dapat melihat bahwa setiap bagian dari logika Anda dapat dihitung dalam fungsi terpisah kemudian disusun bersama. Ini memungkinkan untuk fungsi yang lebih kecil yang biasanya lebih mudah untuk dipecahkan. Sebagai contoh mainan Anda, mungkin ini menambah lebih banyak kerumitan daripada menghilangkannya, tetapi dalam kode dunia nyata fungsi terpisah seringkali jauh lebih sederhana daripada keseluruhan.
sumber
stopAt10
adalah tidak konsumen yang baik. jawaban Anda adalah lebih baik daripada yang Anda mengutip dalam hal itu benar mengisolasi produser dasarscanl (+) 0
nilai-nilai. konsumsi mereka harus memasukkan logika kontrol secara langsung, lebih baik diimplementasikan hanya dengan duaspan
dan satulast
, secara eksplisit. itu akan mengikuti struktur kode dan logika yang asli, juga, dan mudah dipelihara.Sebagian besar contoh pemrosesan daftar Anda akan melihat menggunakan fungsi suka
map
,filter
,sum
dll yang beroperasi pada daftar secara keseluruhan. Tetapi dalam kasus Anda, Anda memiliki keluar awal bersyarat - pola yang agak tidak biasa yang tidak didukung oleh operasi daftar yang biasa. Jadi, Anda perlu menurunkan tingkat abstraksi dan menggunakan rekursi - yang juga lebih dekat dengan bagaimana contoh imperatif terlihat.Ini adalah terjemahan yang agak langsung (mungkin bukan idiomatik) ke Clojure:
Edit: Jules titik bahwa
reduce
di Clojure lakukan mendukung keluar awal. Menggunakan ini lebih elegan:Bagaimanapun, Anda dapat melakukan apa saja dalam bahasa fungsional seperti yang Anda bisa dalam bahasa imperatif, tetapi Anda sering harus mengubah pola pikir Anda untuk menemukan solusi yang elegan. Dalam pengkodean imperatif Anda memikirkan memproses daftar langkah demi langkah, sementara dalam bahasa fungsional Anda mencari operasi untuk diterapkan ke semua elemen dalam daftar.
sumber
reduce
Operasi Clojure mendukung keluar awal.takeWhile
bukan 'operasi umum'?takeWhile
ini adalah operasi yang umum, ini tidak terlalu berguna dalam kasus ini, karena Anda memerlukan hasil transformasi Anda sebelum Anda dapat memutuskan apakah akan berhenti. Dalam bahasa malas ini tidak masalah: Anda dapat menggunakanscan
dantakeWhile
pada hasil pemindaian (lihat jawaban Karl Bielefeldt, yang walaupun tidak digunakantakeWhile
dapat dengan mudah ditulis ulang untuk melakukannya), tetapi untuk bahasa yang ketat seperti clojure ini akan berarti memproses seluruh daftar dan kemudian membuang hasil sesudahnya. Fungsi generator dapat mengatasi ini, dan saya percaya clojure mendukungnya.take-while
di Clojure menghasilkan urutan malas (sesuai dengan dokumen). Cara lain untuk mengatasi ini adalah dengan transduser (mungkin yang terbaik).Seperti yang ditunjukkan oleh jawaban lain, Clojure memiliki
reduced
untuk menghentikan pengurangan lebih awal:Ini adalah solusi terbaik untuk situasi khusus Anda. Anda juga bisa mendapatkan banyak jarak tempuh dari penggabungan
reduced
dengantransduce
, yang memungkinkan Anda menggunakan transduser darimap
,filter
dll. Namun itu jauh dari jawaban lengkap untuk pertanyaan umum Anda.Escape continuations adalah versi umum dari pernyataan break and return. Mereka secara langsung diimplementasikan dalam beberapa Skema (
call-with-escape-continuation
), Common Lisp (block
+return
,catch
+throw
) dan bahkan C (setjmp
+longjmp
). Kelanjutan yang dibatasi atau tidak umum yang lebih umum seperti yang ditemukan dalam Skema standar atau sebagai kelanjutan monad di Haskell dan Scala juga dapat digunakan sebagai kelanjutan pelarian.Misalnya, di Racket Anda dapat menggunakan
let/ec
seperti ini:Banyak bahasa lain juga memiliki konstruksi yang mirip kontinuitas dalam bentuk penanganan pengecualian. Dalam Haskell Anda juga bisa menggunakan salah satu dari berbagai monads kesalahan dengan
foldM
. Karena mereka terutama menangani kesalahan konstruksi menggunakan pengecualian atau kesalahan monads untuk pengembalian awal biasanya tidak dapat diterima secara budaya dan mungkin sangat lambat.Anda juga dapat drop-down dari fungsi tingkat tinggi untuk mengikuti panggilan.
Saat menggunakan loop, Anda memasukkan iterasi berikutnya secara otomatis ketika Anda mencapai ujung tubuh loop. Anda dapat memasukkan iterasi berikutnya lebih awal dengan
continue
atau keluar dari loop denganbreak
(ataureturn
). Saat menggunakan panggilan ekor (atauloop
konstruksi Clojure yang meniru rekursi ekor), Anda harus selalu membuat panggilan eksplisit untuk memasukkan iterasi berikutnya. Untuk berhenti mengulang, Anda tidak melakukan panggilan rekursif tetapi langsung memberikan nilainya:sumber
MonadError
, pada dasarnya setaraEither
tidak memiliki bias terhadap penanganan kesalahan saja, sehingga dapat dengan mudah digunakan sebagai pengganti.Bagian yang rumit adalah loop. Mari kita mulai dengan itu. Sebuah loop biasanya dikonversi ke gaya fungsional dengan mengekspresikan iterasi dengan fungsi tunggal. Iterasi adalah transformasi dari variabel loop.
Berikut ini adalah implementasi fungsional dari loop umum:
Dibutuhkan (nilai awal dari variabel loop, fungsi yang mengekspresikan iterasi tunggal [pada variabel loop]) (suatu kondisi untuk melanjutkan loop).
Contoh Anda menggunakan loop pada array, yang juga rusak. Kemampuan ini dalam bahasa imperatif Anda dimasukkan ke dalam bahasa itu sendiri. Dalam pemrograman fungsional, kemampuan seperti itu biasanya diimplementasikan di tingkat perpustakaan. Berikut ini adalah kemungkinan implementasi
Di dalamnya:
Saya menggunakan pasangan ((val, next_pos)) yang berisi variabel loop terlihat di luar dan posisi dalam array, yang menyembunyikan fungsi ini.
Fungsi iterasi sedikit lebih kompleks daripada di loop umum, versi ini memungkinkan untuk menggunakan elemen array saat ini. [Ini dalam bentuk kari .]
Fungsi seperti itu biasanya dinamai "lipat".
Saya meletakkan "l" dalam nama untuk menunjukkan bahwa akumulasi elemen-elemen array dilakukan dengan cara asosiatif-kiri; untuk meniru kebiasaan bahasa pemrograman imperatif untuk mengulangi array dari indeks rendah ke tinggi.
Saya meletakkan "c" dalam nama untuk menunjukkan bahwa versi lipatan ini mengambil kondisi yang mengontrol jika dan kapan loop harus dihentikan lebih awal.
Tentu saja fungsi utilitas seperti itu mungkin sudah tersedia di perpustakaan dasar yang dikirimkan dengan bahasa pemrograman fungsional yang digunakan. Saya menulisnya di sini untuk demonstrasi.
Sekarang kita memiliki semua alat yang ada dalam bahasa dalam kasus imperatif, kita dapat beralih untuk mengimplementasikan fungsionalitas spesifik dari contoh Anda.
Variabel dalam loop Anda adalah pasangan ('jawaban', boolean yang mengkodekan apakah akan melanjutkan).
Perhatikan bahwa saya menggunakan "variabel" 'new_answer' baru. Ini karena dalam pemrograman fungsional saya tidak dapat mengubah nilai "variabel" yang sudah diinisialisasi. Saya tidak khawatir tentang kinerja, kompiler dapat menggunakan kembali memori 'jawaban' untuk 'new_answer' melalui analisis seumur hidup, jika dianggap lebih efisien.
Memasukkan ini ke dalam fungsi loop kami yang dikembangkan sebelumnya:
"Array" di sini adalah nama modul yang mengekspor fungsi foldlc.
"kepalan", "kedua" singkatan dari fungsi yang mengembalikan komponen pertama, kedua dari parameter pasangannya
Dalam hal ini gaya "point-free" meningkatkan pembacaan implementasi doSomeCalc:
(>>>) adalah komposisi fungsi:
(>>>) : (a -> b) -> (b -> c) -> (a -> c)
Sama seperti di atas, hanya parameter "arr" yang ditinggalkan dari kedua sisi persamaan yang menentukan.
Satu hal terakhir: memeriksa case (array == null). Dalam bahasa pemrograman yang dirancang lebih baik, tetapi bahkan dalam bahasa yang dirancang dengan buruk dengan beberapa disiplin dasar orang lebih suka menggunakan jenis opsional untuk mengekspresikan ketidak-ada-an. Ini tidak ada hubungannya dengan pemrograman fungsional, yang akhirnya menjadi pertanyaan, jadi saya tidak menghadapinya.
sumber
Pertama, tulis ulang loop sedikit, sehingga setiap iterasi loop keluar lebih awal, atau bermutasi
answer
tepat sekali:Seharusnya jelas bahwa perilaku versi ini persis sama dengan sebelumnya, tetapi sekarang, jauh lebih mudah untuk mengkonversi ke gaya rekursif. Ini terjemahan Haskell langsung:
Sekarang ini berfungsi murni, tetapi kita bisa memperbaikinya dari sudut pandang efisiensi dan keterbacaan dengan menggunakan lipatan alih-alih rekursi eksplisit:
Dalam konteks ini,
Left
awal keluar dengan nilainya, danRight
melanjutkan rekursi dengan nilainya.Ini sekarang dapat disederhanakan sedikit lebih, seperti ini:
Ini lebih baik sebagai kode Haskell akhir, tetapi sekarang agak kurang jelas bagaimana memetakan kembali ke Jawa asli.
sumber