Pemrograman Fungsional - Kekekalan

12

Saya mencoba untuk memahami berurusan dengan data yang tidak dapat diubah dalam FP (khususnya dalam F #, tetapi FP lain juga ok) dan menghentikan kebiasaan lama berpikir penuh negara (gaya OOP). Sebagian dari jawaban yang dipilih untuk pertanyaan di sini mengulangi pencarian saya untuk setiap tulisan di sekitar masalah yang diselesaikan oleh representasi stateful dalam OOP dengan yang tidak berubah dalam FP (Sebagai contoh: Antrian dengan Produsen & Konsumen). Ada pemikiran atau tautan yang disambut? Terima kasih sebelumnya.

Sunting : Untuk lebih memperjelas pertanyaan, bagaimana struktur yang tidak berubah (mis: antrian) dibagikan secara bersamaan di beberapa utas (mis: produsen dan konsumen) di FP

venkram
sumber
Salah satu cara untuk menangani masalah konkurensi adalah dengan membuat salinan antrian setiap kali (agak mahal, tetapi berfungsi).
Pekerjaan
infoq.com/presentations/Functional-Data-Structures-in-Scala Anda mungkin menemukan speach ini berwawasan luas.
deadalnix

Jawaban:

19

Meskipun kadang-kadang dinyatakan seperti itu, pemrograman fungsional functional tidak mencegah perhitungan stateful. Apa yang dilakukannya adalah memaksa programmer untuk membuat negara eksplisit.

Sebagai contoh, mari kita ambil struktur dasar dari beberapa program menggunakan antrian imperatif (dalam beberapa bahasa):

q := Queue.new();
while (true) {
    if (Queue.is_empty(q)) {
        Queue.add(q, producer());
    } else {
        consumer(Queue.take(q));
    }
}

Struktur yang sesuai dengan struktur data antrian fungsional (masih dalam bahasa imperatif, sehingga untuk mengatasi satu perbedaan pada suatu waktu) akan terlihat seperti ini:

q := Queue.empty;
while (true) {
    if (q = Queue.empty) {
        q := Queue.add(q, producer());
    } else {
        (tail, element) := Queue.take(q);
        consumer(element);
        q := tail;
    }
}

Karena antrian sekarang tidak berubah, objek itu sendiri tidak berubah. Dalam pseudo-code ini, qitu sendiri adalah variabel; tugas q := Queue.add(…)dan q := tailmembuatnya menunjuk ke objek yang berbeda. Antarmuka fungsi antrian telah berubah: masing-masing harus mengembalikan objek antrian baru yang dihasilkan dari operasi.

Dalam bahasa murni fungsional, yaitu dalam bahasa tanpa efek samping, Anda harus membuat semua status eksplisit. Karena produsen dan konsumen mungkin melakukan sesuatu, negara mereka juga harus berada di antarmuka penelepon mereka di sini.

main_loop(q, other_state) {
    if (q = Queue.empty) {
        let (new_state, element) = producer(other_state);
        main_loop(Queue.add(q, element), new_state);
    } else {
        let (tail, element) = Queue.take(q);
        let new_state = consumer(other_state, element);
        main_loop(tail, new_state);
    }
}
main_loop(Queue.empty, initial_state)

Perhatikan bagaimana sekarang setiap bagian dari negara dikelola secara eksplisit. Fungsi manipulasi antrian mengambil antrian sebagai input dan menghasilkan antrian baru sebagai output. Produser dan konsumen melewati negara mereka juga.

Pemrograman konkuren tidak cocok dengan baik di dalam pemrograman fungsional, tapi itu sangat cocok di sekitar pemrograman fungsional. Idenya adalah untuk menjalankan sekelompok node komputasi yang terpisah dan membiarkan mereka bertukar pesan. Setiap node menjalankan program fungsional, dan keadaannya berubah saat ia mengirim dan menerima pesan.

Melanjutkan contoh, karena ada satu antrian, dikelola oleh satu simpul tertentu. Konsumen mengirim pesan kepada simpul itu untuk mendapatkan elemen. Produsen mengirim pesan ke simpul itu untuk menambahkan elemen.

main_loop(q) =
    consumer->consume(q->take()) || q->add(producer->produce());
    main_loop(q)

Bahasa "industrialisasi" yang mendapat konkurensi benar³ adalah Erlang . Mempelajari Erlang jelas merupakan jalan menuju pencerahan⁴ tentang pemrograman bersamaan.

Semua orang beralih ke bahasa bebas efek samping sekarang!

¹ Istilah ini memiliki beberapa arti; di sini saya pikir Anda menggunakannya berarti pemrograman tanpa efek samping, dan itulah artinya saya juga menggunakan.
² Pemrograman dengan status implisit adalah pemrograman imperatif ; orientasi objek adalah perhatian yang sepenuhnya ortogonal.
³ Peradangan, saya tahu, tetapi saya bersungguh-sungguh. Utas dengan memori bersama adalah bahasa rakitan pemrograman bersamaan. Pesan lewat jauh lebih mudah untuk dipahami, dan kurangnya efek samping benar-benar bersinar segera setelah Anda memperkenalkan konkurensi.
Dan ini datang dari seseorang yang bukan penggemar Erlang, tetapi karena alasan lain.

Gilles 'SANGAT berhenti menjadi jahat'
sumber
2
+1 Jawaban yang jauh lebih lengkap, meskipun saya kira orang bisa berdalih bahwa Erlang bukan bahasa FP murni.
Rein Henrichs
1
@Rein Henrichs: Memang. Bahkan, dari semua bahasa utama yang ada saat ini, Erlang adalah yang paling setia menerapkan Orientasi Objek.
Jörg W Mittag
2
@ Jörg Setuju. Meskipun, sekali lagi, orang dapat berdalih bahwa FP dan OO murni bersifat ortogonal.
Rein Henrichs
Jadi, untuk mengimplementasikan antrian yang tidak dapat diubah dalam perangkat lunak bersamaan, pesan perlu dikirim dan diterima antar node. Di mana pesan tertunda disimpan?
mouviciel
@mouviciel Elemen antrian disimpan dalam antrian pesan masuk node. Fasilitas antrian pesan ini adalah fitur dasar dari infrastruktur terdistribusi. Desain infrastruktur alternatif yang berfungsi baik untuk konkurensi lokal tetapi tidak dengan sistem terdistribusi adalah untuk memblokir pengirim sampai penerima siap. Saya menyadari ini tidak menjelaskan semuanya, perlu satu atau dua bab dari buku tentang pemrograman bersamaan untuk menjelaskan hal ini sepenuhnya.
Gilles 'SO- stop being evil'
4

Perilaku stateful dalam bahasa FP diterapkan sebagai transformasi dari negara sebelumnya ke negara baru. Misalnya, enqueue akan menjadi transformasi dari antrian dan nilai ke antrian baru dengan nilai enqueued. Dequeue akan menjadi transformasi dari antrian ke nilai dan antrian baru dengan nilai dihapus. Konstruk seperti monad telah dirancang untuk abstrak transformasi keadaan ini (dan hasil komputasi lainnya) dengan cara yang bermanfaat

Rein Henrichs
sumber
3
Jika ini adalah antrian baru untuk setiap operasi tambah / hapus, bagaimana dua (atau lebih) operasi async (utas) berbagi antrian? Apakah ini merupakan pola untuk mengabstraksi antrian yang baru?
venkram
Concurrency adalah pertanyaan yang sangat berbeda. Saya tidak dapat memberikan jawaban yang memadai dalam komentar.
Rein Henrichs
2
@Rein Henrichs: "tidak dapat memberikan jawaban yang memadai dalam komentar". Itu biasanya berarti Anda harus memperbarui jawaban untuk mengatasi masalah terkait komentar.
S.Lott
Konkurensi juga bisa monadik, lihat haskell Control.Concurrency.STM.
alternatif
1
@ S.Lott dalam hal ini berarti OP harus mengajukan pertanyaan baru. Concurrency adalah OT untuk pertanyaan ini, yaitu tentang struktur data yang tidak dapat diubah.
Rein Henrichs
2

... masalah yang diselesaikan oleh representasi stateful di OOP dengan yang tidak dapat diubah di FP (Misalnya: Antrian dengan Produsen & Konsumen)

Pertanyaan Anda adalah apa yang disebut "masalah XY". Secara khusus, konsep yang Anda kutip (antrian dengan produsen & konsumen) sebenarnya adalah solusi dan bukan "masalah" seperti yang Anda gambarkan. Ini menimbulkan kesulitan karena Anda meminta implementasi fungsional murni dari sesuatu yang secara inheren tidak murni. Jadi jawaban saya dimulai dengan pertanyaan: apa masalah yang Anda coba selesaikan?

Ada banyak cara bagi banyak produsen untuk mengirim hasilnya ke satu konsumen yang dibagikan. Mungkin solusi yang paling jelas dalam F # adalah menjadikan konsumen sebagai agen (alias MailboxProcessor) dan meminta para produsen Posthasilnya ke agen konsumen. Ini menggunakan antrian internal dan itu tidak murni (mengirim pesan dalam F # adalah efek samping yang tidak terkendali, pengotor).

Namun, sangat mungkin bahwa masalah yang mendasarinya adalah sesuatu yang lebih seperti pola sebar-kumpul dari pemrograman paralel. Untuk mengatasi masalah ini, Anda dapat membuat array nilai input lalu Array.Parallel.mapmengatasinya dan mengumpulkan hasilnya menggunakan serial Array.reduce. Atau, Anda dapat menggunakan fungsi dari PSeqmodul untuk memproses elemen-elemen urutan secara paralel.

Saya juga harus menekankan bahwa tidak ada yang salah dengan pemikiran negara. Kemurnian memiliki kelebihan tetapi tentu saja bukan obat mujarab dan Anda harus membuat diri Anda menyadari kekurangannya juga. Memang, inilah tepatnya mengapa F # bukan bahasa fungsional murni: jadi Anda dapat menggunakan pengotor ketika mereka lebih disukai.

Jon Harrop
sumber
1

Clojure memiliki konsep negara dan identitas yang dipikirkan dengan sangat baik, yang terkait erat dengan konkurensi. Kekekalan memainkan peran penting, semua nilai dalam Clojure tidak dapat diubah, dan dapat diakses melalui referensi. Referensi lebih dari sekadar petunjuk sederhana. Mereka mengelola akses ke nilai, dan ada beberapa jenis dengan semantik yang berbeda. Referensi dapat dimodifikasi untuk menunjukkan nilai baru (tidak berubah), dan perubahan semacam itu dijamin bersifat atomik. Namun, setelah modifikasi semua utas lainnya masih bekerja pada nilai asli, setidaknya sampai mereka mengakses referensi lagi.

Saya sangat menyarankan Anda untuk membaca artikel yang bagus tentang status dan identitas di Clojure , ini menjelaskan rinciannya jauh lebih baik daripada yang saya bisa.

Adam Byrtek
sumber