ฟังก์ชั่นการเขียนโปรแกรม - Immutability


12

ฉันพยายามที่จะเข้าใจการจัดการกับข้อมูลที่ไม่เปลี่ยนรูปแบบใน FP (โดยเฉพาะใน F # แต่ FP อื่น ๆ ก็โอเคเช่นกัน) และทำลายนิสัยเดิมของการคิดแบบเต็มรัฐ (OOP) ส่วนหนึ่งของคำตอบที่เลือกสำหรับคำถามที่นี่ย้ำการค้นหาของฉันสำหรับการแก้ไขปัญหาที่แก้ไขโดยการเป็นตัวแทนใน OOP กับสิ่งที่ไม่เปลี่ยนรูปใน FP (ตัวอย่างเช่น: คิวกับ Producers & Consumer) ความคิดหรือลิงค์ยินดีต้อนรับ? ขอบคุณล่วงหน้า.

แก้ไข : หากต้องการเพิ่มความกระจ่างของคำถามอีกเล็กน้อยโครงสร้างที่ไม่เปลี่ยนรูปแบบ (เช่นคิว) จะถูกแชร์กันในหลาย ๆ เธรดพร้อมกัน (เช่นผู้ผลิตและผู้บริโภค) ใน FP ได้อย่างไร


วิธีหนึ่งในการจัดการปัญหาการเกิดพร้อมกันคือการทำสำเนาของคิวในแต่ละครั้ง (ค่อนข้างแพง แต่ใช้งานได้)
งาน

infoq.com/presentations/Functional-Data-Structures-in-Scalaคุณอาจพบว่าการพูดนี้เป็นไปอย่างชาญฉลาด
deadalnix

คำตอบ:


19

แม้ว่าบางครั้งมันจะแสดงออกมาอย่างนั้นการเขียนโปรแกรมเพื่อการใช้งานก็ไม่ได้ป้องกันการคำนวณแบบไร้รัฐ มันทำอะไรคือบังคับให้โปรแกรมเมอร์ทำให้สถานะชัดเจน

ตัวอย่างเช่นลองใช้โครงสร้างพื้นฐานของบางโปรแกรมโดยใช้คิวที่จำเป็น (ในบาง pseudolanguage):

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

โครงสร้างที่สอดคล้องกับโครงสร้างข้อมูลคิวการทำงาน (ยังอยู่ในภาษาที่จำเป็นเพื่อจัดการกับความแตกต่างในแต่ละครั้ง) จะมีลักษณะเช่นนี้:

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

เนื่องจากคิวไม่สามารถเปลี่ยนแปลงได้ในขณะนี้วัตถุจึงไม่เปลี่ยนแปลง ในโค้ดหลอกนี้qตัวเองเป็นตัวแปร การมอบหมายq := Queue.add(…)และq := tailทำให้มันชี้ไปที่วัตถุอื่น อินเทอร์เฟซของฟังก์ชันคิวมีการเปลี่ยนแปลง: แต่ละรายการต้องส่งคืนวัตถุคิวใหม่ที่เป็นผลมาจากการดำเนินการ

ในภาษาที่ใช้งานได้จริงเช่นในภาษาที่ไม่มีผลข้างเคียงคุณต้องทำให้ทุกสถานะชัดเจน เนื่องจากผู้ผลิตและผู้บริโภคคาดว่าจะทำอะไรบางอย่างสถานะของพวกเขาจะต้องอยู่ในอินเทอร์เฟซผู้โทรของพวกเขาที่นี่เช่นกัน

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)

ทราบว่าตอนนี้ทุกส่วนของรัฐมีการจัดการอย่างชัดเจน ฟังก์ชันการจัดการคิวใช้คิวเป็นอินพุตและสร้างคิวใหม่เป็นเอาต์พุต ผู้ผลิตและผู้บริโภคผ่านสถานะของพวกเขาเช่นกัน

การเขียนโปรแกรมพร้อมกันไม่พอดีให้ดีภายในโปรแกรมการทำงาน แต่มันเหมาะกับเป็นอย่างดีรอบการทำงานการเขียนโปรแกรม แนวคิดคือรันโหนดการคำนวณแยกต่างหากและให้พวกเขาแลกเปลี่ยนข้อความ แต่ละโหนดจะรันโปรแกรมที่ใช้งานได้และสถานะจะเปลี่ยนไปในขณะที่ส่งและรับข้อความ

ดำเนินการต่อตัวอย่างเนื่องจากมีคิวเดียวจึงถูกจัดการโดยหนึ่งโหนด ผู้บริโภคส่งข้อความถึงโหนดเพื่อรับองค์ประกอบ ผู้ผลิตส่งข้อความถึงโหนดเพื่อเพิ่มองค์ประกอบ

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

หนึ่ง“อุตสาหกรรม” ภาษาที่ได้รับright³พร้อมกันคือErlang การเรียนรู้ Erlang เป็นเส้นทางสู่การตรัสรู้อย่างแน่นอนเกี่ยวกับการเขียนโปรแกรมพร้อมกัน

ตอนนี้ทุกคนเปลี่ยนไปใช้ภาษาที่ไม่มีผลข้างเคียง!

¹ คำนี้มีความหมายหลายแห่ง ที่นี่ฉันคิดว่าคุณกำลังใช้มันเพื่อหมายถึงการเขียนโปรแกรมโดยไม่มีผลข้างเคียงและนั่นคือความหมายที่ฉันใช้ด้วย
² Programming กับรัฐโดยนัยคือการเขียนโปรแกรมจำเป็น ; การวางแนววัตถุเป็นสิ่งที่ต้องคำนึงถึงอย่างสมบูรณ์
³ การอักเสบฉันรู้ แต่ฉันหมายถึงมัน เธรดที่มีหน่วยความจำแบบแบ่งใช้เป็นภาษาแอสเซมบลีของการเขียนโปรแกรมพร้อมกัน การส่งข้อความเป็นเรื่องที่เข้าใจได้ง่ายกว่ามากและการไม่มีผลข้างเคียงจะส่องสว่างทันทีที่คุณแนะนำการใช้งานพร้อมกัน
และนี่มาจากคนที่ไม่ใช่แฟนของ Erlang แต่ด้วยเหตุผลอื่น


2
+1 คำตอบที่สมบูรณ์มากขึ้นแม้ว่าฉันคิดว่าหนึ่งอาจพูดได้ว่า Erlang ไม่ใช่ภาษา FP บริสุทธิ์
Rein Henrichs

1
@ Rein Henrichs: แน่นอน ในความเป็นจริงจากภาษากระแสหลักที่มีอยู่ในปัจจุบันทั้งหมด Erlang เป็นภาษาที่ใช้การวางแนวของวัตถุอย่างซื่อสัตย์ที่สุด
Jörg W Mittag

2
@ Jörgเห็นด้วย แม้ว่าอีกครั้งหนึ่งอาจพูดเล่นอย่างนั้น FP บริสุทธิ์และ OO เป็น orthogonal
Rein Henrichs

ดังนั้นสำหรับการใช้คิวที่ไม่เปลี่ยนรูปในซอฟต์แวร์ที่เกิดขึ้นพร้อมกันจำเป็นต้องมีการส่งและรับข้อความระหว่างโหนด ที่เก็บข้อความที่ค้างอยู่ที่ไหน
mouviciel

องค์ประกอบ @mouviciel Queue ถูกเก็บไว้ในคิวข้อความขาเข้าของโหนด สิ่งอำนวยความสะดวกการจัดคิวข้อความนี้เป็นคุณสมบัติพื้นฐานของโครงสร้างพื้นฐานแบบกระจาย การออกแบบโครงสร้างพื้นฐานทางเลือกซึ่งทำงานได้ดีสำหรับการทำงานพร้อมกันในท้องถิ่น แต่ไม่ใช่กับระบบแบบกระจายคือการบล็อกผู้ส่งจนกว่าผู้รับจะพร้อม ฉันรู้ว่านี่ไม่ได้อธิบายทุกอย่างมันต้องใช้บทหรือสองเล่มในการเขียนโปรแกรมพร้อมกันเพื่ออธิบายสิ่งนี้อย่างเต็มที่
Gilles 'หยุดชั่วร้าย'

4

พฤติกรรมแบบ Stateful ใน FP กังนั้นถูกนำมาใช้เป็นการเปลี่ยนแปลงจากสถานะก่อนหน้าสู่สถานะใหม่ ตัวอย่างเช่นการเข้าคิวจะเป็นการแปลงจากคิวและค่าเป็นคิวใหม่ที่มีค่าเข้าคิว Dequeue จะเป็นการแปลงจากคิวเป็นค่าและคิวใหม่ที่ลบค่า โครงสร้างเช่นพระได้รับการออกแบบให้เป็นนามธรรมการเปลี่ยนแปลงสถานะนี้ (และผลการคำนวณอื่น ๆ ) ในรูปแบบที่มีประโยชน์


3
หากเป็นคิวใหม่สำหรับการดำเนินการเพิ่ม / ลบทุกการดำเนินการ async (เธรด) สองรายการ (หรือมากกว่า) จะแชร์คิวได้อย่างไร มันเป็นรูปแบบที่เป็นนามธรรมเกี่ยวกับการสร้างคิวใหม่หรือไม่?
venkram

การเห็นพ้องด้วยกันเป็นคำถามที่แตกต่างอย่างสิ้นเชิง ฉันไม่สามารถให้คำตอบที่เพียงพอในความคิดเห็น
Rein Henrichs

2
@Rein Henrichs: "ไม่สามารถให้คำตอบที่เพียงพอในความคิดเห็น" โดยปกติแล้วหมายความว่าคุณควรอัปเดตคำตอบเพื่อแก้ไขปัญหาเกี่ยวกับความคิดเห็น
S.Lott

การเห็นพ้องด้วยสามารถเป็น monadic ได้เช่นกันดูที่ haskells Control.Concurrency.STM
ทางเลือก

1
@ S.Lott ในกรณีนี้หมายความว่า OP ควรถามคำถามใหม่ การเกิดขึ้นพร้อมกันคือ OT สำหรับคำถามนี้ซึ่งเกี่ยวกับโครงสร้างข้อมูลที่เปลี่ยนแปลงไม่ได้
Rein Henrichs

2

... ปัญหาที่แก้ไขได้โดยการนำเสนอที่เป็นรัฐใน OOP กับสิ่งที่เปลี่ยนแปลงไม่ได้ใน FP (ตัวอย่างเช่น: คิวที่มีโปรดิวเซอร์และผู้บริโภค)

คำถามของคุณคือสิ่งที่เรียกว่า "ปัญหา XY" โดยเฉพาะแนวคิดที่คุณอ้างถึง (คิวกับผู้ผลิตและผู้บริโภค) เป็นวิธีแก้ปัญหาและไม่ใช่ "ปัญหา" ตามที่คุณอธิบาย สิ่งนี้นำเสนอปัญหาเนื่องจากคุณกำลังขอให้มีการใช้งานสิ่งที่ไม่บริสุทธิ์อย่างแท้จริง ดังนั้นคำตอบของฉันเริ่มต้นด้วยคำถาม: ปัญหาที่คุณพยายามแก้ไขคืออะไร

มีหลายวิธีที่ผู้ผลิตหลายรายจะส่งผลลัพธ์ไปยังผู้บริโภคที่ใช้ร่วมกันเพียงคนเดียว บางทีทางออกที่ชัดเจนที่สุดใน F # คือการทำให้ผู้บริโภคเป็นตัวแทน (aka MailboxProcessor) และมีผู้ผลิตPostผลลัพธ์ของพวกเขาไปยังตัวแทนผู้บริโภค สิ่งนี้ใช้คิวภายในและไม่บริสุทธิ์ (การส่งข้อความใน F # เป็นผลข้างเคียงที่ไม่สามารถควบคุมได้คือมลทิน)

อย่างไรก็ตามมีโอกาสค่อนข้างมากที่ปัญหาพื้นฐานคือบางอย่างเช่นรูปแบบการกระจายที่รวบรวมจากการเขียนโปรแกรมแบบขนาน เพื่อแก้ปัญหานี้คุณอาจสร้างอาร์เรย์ของค่าการป้อนข้อมูลแล้วเหนือพวกเขาและรวบรวมผลการใช้อนุกรมArray.Parallel.map Array.reduceหรือคุณอาจใช้ฟังก์ชั่นจากPSeqโมดูลเพื่อประมวลผลองค์ประกอบของลำดับในแบบคู่ขนาน

ฉันควรจะเน้นว่าไม่มีอะไรผิดปกติกับการคิดแบบไร้รัฐ ความบริสุทธิ์มีข้อดี แต่ก็ไม่ใช่ยาครอบจักรวาลและคุณควรตระหนักถึงข้อบกพร่องด้วยเช่นกัน นี่คือเหตุผลที่ F # ไม่ใช่ภาษาที่ใช้งานได้จริง: ดังนั้นคุณสามารถใช้สิ่งสกปรกเมื่อพวกเขาต้องการ


1

Clojure มีแนวคิดเกี่ยวกับสถานะและอัตลักษณ์ที่ดีมากซึ่งเกี่ยวข้องกับการเกิดพร้อมกัน Immutability มีบทบาทสำคัญค่าทั้งหมดใน Clojure ไม่เปลี่ยนรูปและสามารถเข้าถึงได้ผ่านการอ้างอิง การอ้างอิงเป็นมากกว่าตัวชี้ธรรมดา พวกเขาจัดการการเข้าถึงค่าและมีหลายประเภทด้วยความหมายที่แตกต่างกัน การอ้างอิงสามารถแก้ไขให้ชี้ไปที่ค่าใหม่ (ไม่เปลี่ยนรูป) และการเปลี่ยนแปลงดังกล่าวรับประกันว่าเป็นอะตอม อย่างไรก็ตามหลังจากการแก้ไขเธรดอื่น ๆ ทั้งหมดยังคงใช้งานได้กับค่าดั้งเดิมอย่างน้อยก็จนกว่าพวกเขาจะเข้าถึงการอ้างอิงอีกครั้ง

ฉันขอแนะนำให้คุณอ่านบทความที่ยอดเยี่ยมเกี่ยวกับสถานะและเอกลักษณ์ใน Clojureอธิบายรายละเอียดได้ดีกว่าที่ฉันสามารถทำได้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.