ความหมายของการเขียนโปรแกรมฟังก์ชั่น
การแนะนำThe Joy of Clojureกล่าวว่า:
ฟังก์ชั่นการเขียนโปรแกรมเป็นหนึ่งในเงื่อนไขการคำนวณเหล่านั้นที่มีความหมายอสัณฐาน หากคุณขอคำอธิบายจากโปรแกรมเมอร์ 100 คนคุณจะได้รับคำตอบที่แตกต่างกัน 100 คำ ...
ฟังก์ชั่นการเขียนโปรแกรมที่เกี่ยวข้องและอำนวยความสะดวกในการใช้งานและองค์ประกอบของฟังก์ชั่น ... สำหรับภาษาที่จะได้รับการพิจารณาการทำงานความคิดของฟังก์ชั่นจะต้องเป็นชั้นหนึ่ง ฟังก์ชั่นชั้นหนึ่งสามารถจัดเก็บส่งผ่านและส่งคืนเช่นเดียวกับข้อมูลอื่น ๆ นอกเหนือจากแนวคิดหลักนี้ [คำจำกัดความของ FP อาจรวมถึง] ความบริสุทธิ์ความผันแปรไม่ได้การเรียกซ้ำความเกียจคร้านและความโปร่งใสในการอ้างอิง
การเขียนโปรแกรมใน Scala 2nd Edition p. 10 มีคำจำกัดความต่อไปนี้:
ฟังก์ชั่นการเขียนโปรแกรมถูกชี้นำโดยสองแนวคิดหลัก ความคิดแรกคือฟังก์ชั่นเป็นค่าที่ดีเลิศ ... คุณสามารถส่งผ่านฟังก์ชั่นเป็นอาร์กิวเมนต์ไปยังฟังก์ชั่นอื่น ๆ คืนพวกเขาเป็นผลมาจากฟังก์ชั่นหรือเก็บไว้ในตัวแปร ...
แนวคิดหลักที่สองของการเขียนโปรแกรมฟังก์ชั่นคือการทำงานของโปรแกรมควรแมปค่าอินพุตกับค่าเอาต์พุตแทนที่จะเปลี่ยนข้อมูล
หากเรายอมรับข้อกำหนดแรกสิ่งเดียวที่คุณต้องทำเพื่อให้โค้ดของคุณ "ใช้งานได้" คือการเปลี่ยนลูปของคุณให้อยู่ข้างใน คำจำกัดความที่สองรวมถึงการไม่เปลี่ยนรูป
ฟังก์ชั่นชั้นหนึ่ง
ลองนึกภาพตอนนี้คุณได้รับรายชื่อผู้โดยสารจากวัตถุรถบัสของคุณและคุณย้ำไปเรื่อย ๆ เพื่อลดบัญชีธนาคารของผู้โดยสารแต่ละคนตามจำนวนค่าโดยสาร วิธีการทำงานในการดำเนินการเดียวกันนี้จะต้องมีวิธีการในรถบัสอาจเรียกว่า forEachPassenger ที่ใช้ฟังก์ชั่นหนึ่งอาร์กิวเมนต์ จากนั้นรถบัสจะย้ำเหนือผู้โดยสาร แต่ก็ทำได้ดีที่สุดและรหัสลูกค้าของคุณที่เรียกเก็บค่าโดยสารสำหรับการขี่จะถูกนำไปใช้งานและส่งผ่านไปยังผู้โดยสารแต่ละคน Voila! คุณกำลังใช้งานโปรแกรมมิง
ความจำเป็น:
for (Passenger p : Bus.getPassengers()) {
p.debit(fare);
}
ใช้งานได้ (ใช้ฟังก์ชั่นนิรนามหรือ "lambda" ใน Scala):
myBus = myBus.forEachPassenger(p:Passenger -> { p.debit(fare) })
รุ่น Scala หวานมากขึ้น:
myBus = myBus.forEachPassenger(_.debit(fare))
ฟังก์ชั่นที่ไม่ใช่ชั้นแรก
หากภาษาของคุณไม่รองรับฟังก์ชั่นชั้นหนึ่งสิ่งนี้ก็น่าเกลียดมาก ใน Java 7 หรือรุ่นก่อนหน้าคุณต้องจัดเตรียมส่วนต่อประสาน "หน้าที่วัตถุ" ดังนี้:
// Java 8 has java.util.function.Consumer, but in earlier
// versions you have to roll your own:
public interface Consumer<T> {
public void accept(T t);
}
ดังนั้นคลาส Bus จะมีตัววนซ้ำภายใน:
public void forEachPassenger(Consumer<Passenger> c) {
for (Passenger p : passengers) {
c.accept(p);
}
}
ในที่สุดคุณก็ผ่านวัตถุฟังก์ชั่นที่ไม่ระบุชื่อไปที่รถบัส:
// Java 8 has syntactic sugar to make this look more like
// the Scala solution, but earlier versions require manually
// instantiating a "Function Object," in this case, a
// Consumer:
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
}
}
Java 8 อนุญาตให้ตัวแปรโลคัลถูกดักจับขอบเขตของฟังก์ชันที่ไม่ระบุชื่อ แต่ในเวอร์ชันก่อนหน้า varibales ใด ๆ นั้นต้องถูกประกาศให้เป็นที่สุด หากต้องการหลีกเลี่ยงสิ่งนี้คุณอาจต้องสร้างคลาส wrapper MutableReference นี่คือคลาสเฉพาะจำนวนเต็มที่ให้คุณเพิ่มตัวนับลูปในโค้ดด้านบน:
public static class MutableIntWrapper {
private int i;
private MutableIntWrapper(int in) { i = in; }
public static MutableIntWrapper ofZero() {
return new MutableIntWrapper(0);
}
public int value() { return i; }
public void increment() { i++; }
}
final MutableIntWrapper count = MutableIntWrapper.ofZero();
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
count.increment();
}
}
System.out.println(count.value());
แม้จะมีความอัปลักษณ์นี้บางครั้งมันก็เป็นประโยชน์ในการกำจัดตรรกะที่ซับซ้อนและซ้ำ ๆ จากลูปที่แพร่กระจายไปทั่วโปรแกรมของคุณ
ความอัปลักษณ์นี้ได้รับการแก้ไขใน Java 8 แต่การจัดการข้อยกเว้นที่ตรวจสอบภายในฟังก์ชั่นชั้นหนึ่งยังคงน่าเกลียดมากและ Java ยังคงมีข้อสันนิษฐานของความไม่แน่นอนในคอลเลกชันทั้งหมด ซึ่งนำเราไปสู่เป้าหมายอื่น ๆ ที่มักเกี่ยวข้องกับ FP:
การเปลี่ยนไม่ได้
รายการของ Josh Bloch ที่ 13 คือ "ชอบไม่ได้" แม้จะมีถังขยะทั่วไปพูดคุยกัน OOP สามารถทำได้ด้วยวัตถุที่ไม่เปลี่ยนรูปแบบและการทำเช่นนั้นทำให้ดีขึ้นมาก ตัวอย่างเช่น String ใน Java ไม่เปลี่ยนรูป StringBuffer, OTOH จะต้องไม่แน่นอนเพื่อสร้างสตริงที่ไม่เปลี่ยนรูป งานบางอย่างเช่นการทำงานกับบัฟเฟอร์ต้องการความไม่แน่นอน
ความบริสุทธิ์
แต่ละฟังก์ชั่นควรจะสามารถจดจำได้อย่างน้อย - ถ้าคุณให้พารามิเตอร์อินพุตเดียวกัน (และไม่ควรมีอินพุตนอกเหนือจากอาร์กิวเมนต์ที่เกิดขึ้นจริง) มันควรสร้างเอาต์พุตเดียวกันทุกครั้งโดยไม่ก่อให้เกิด "ผลข้างเคียง" เช่นการเปลี่ยนสถานะโลก / O หรือโยนข้อยกเว้น
มีคนกล่าวว่าในการเขียนโปรแกรมฟังก์ชั่น "มักจะมีความชั่วร้ายบางอย่างเพื่อให้งานสำเร็จ" โดยทั่วไปความบริสุทธิ์ 100% ไม่ใช่เป้าหมาย การลดผลข้างเคียงคือ
ข้อสรุป
จากแนวคิดทั้งหมดข้างต้นการเปลี่ยนแปลงไม่ได้เป็นชัยชนะครั้งเดียวที่ยิ่งใหญ่ที่สุดในแง่ของการใช้งานจริงเพื่อทำให้โค้ดของฉันง่ายขึ้นไม่ว่าจะเป็น OOP หรือ FP การส่งผ่านฟังก์ชั่นการวนซ้ำเป็นชัยชนะครั้งที่สองที่ยิ่งใหญ่ เอกสาร Java 8 Lambdasมีคำอธิบายที่ดีที่สุดของเหตุผล การเรียกซ้ำเป็นสิ่งที่ยอดเยี่ยมสำหรับการประมวลผลต้นไม้ ความเกียจคร้านช่วยให้คุณสามารถทำงานกับคอลเลกชันที่ไม่มีที่สิ้นสุด
ถ้าคุณชอบ JVM ฉันขอแนะนำให้คุณดูที่ Scala และ Clojure ทั้งสองเป็นการตีความเชิงฟังก์ชันของ Programming Programming อย่างลึกซึ้ง Scala เป็นประเภทที่ปลอดภัยที่มีไวยากรณ์คล้าย C แต่จริงๆแล้วมันมีไวยากรณ์ที่เหมือนกันมากกับ Haskell เช่นเดียวกับ C. Clojure ไม่ได้เป็นประเภทที่ปลอดภัยและมันเป็นเสียงกระเพื่อม ฉันเพิ่งโพสต์การเปรียบเทียบ Java, Scala และ Clojureเกี่ยวกับปัญหา refactoring เฉพาะหนึ่ง การเปรียบเทียบของ Logan Campbellโดยใช้ Game of Life นั้นรวมถึง Haskell และพิมพ์ Clojure ด้วย
PS
จิมมี่ฮอฟฟาชี้ให้เห็นว่าคลาสรถบัสของฉันไม่แน่นอน แทนที่จะแก้ไขต้นฉบับฉันคิดว่าสิ่งนี้จะแสดงให้เห็นอย่างชัดเจนถึงประเภทของการปรับโครงสร้างคำถามนี้ที่เกี่ยวกับ สิ่งนี้สามารถแก้ไขได้โดยการทำแต่ละวิธีบนบัสโรงงานเพื่อผลิตรถบัสใหม่แต่ละวิธีสำหรับผู้โดยสารที่เป็นโรงงานเพื่อผลิตผู้โดยสารใหม่ ดังนั้นฉันได้เพิ่มประเภทกลับไปทุกสิ่งซึ่งหมายความว่าฉันจะคัดลอก java.util.function.Function ของ Java 8 แทนส่วนต่อประสานผู้บริโภค
public interface Function<T,R> {
public R apply(T t);
// Note: I'm leaving out Java 8's compose() method here for simplicity
}
จากนั้นบนรถบัส:
public Bus mapPassengers(Function<Passenger,Passenger> c) {
// I have to use a mutable collection internally because Java
// does not have immutable collections that return modified copies
// of themselves the way the Clojure and Scala collections do.
List<Passenger> newPassengers = new ArrayList(passengers.size());
for (Passenger p : passengers) {
newPassengers.add(c.apply(p));
}
return Bus.of(driver, Collections.unmodifiableList(passengers));
}
ในที่สุดวัตถุฟังก์ชั่นที่ไม่ระบุชื่อจะคืนค่าสถานะของสิ่งต่าง ๆ (รถบัสใหม่พร้อมผู้โดยสารใหม่) สิ่งนี้ถือว่า p.debit () ส่งคืนผู้โดยสารที่ไม่เปลี่ยนรูปแบบใหม่ด้วยเงินน้อยกว่าเดิม:
Bus b = b.mapPassengers(new Function<Passenger,Passenger>() {
@Override
public Passenger apply(final Passenger p) {
return p.debit(fare);
}
}
หวังว่าตอนนี้คุณสามารถตัดสินใจด้วยตัวเองว่าคุณต้องการฟังก์ชั่นการใช้ภาษาที่จำเป็นและตัดสินใจได้อย่างไรว่าจะเป็นการดีกว่าที่จะออกแบบโครงการของคุณใหม่โดยใช้ภาษาที่ใช้งานได้ ใน Scala หรือ Clojure คอลเล็กชันและ API อื่น ๆ ได้รับการออกแบบมาเพื่อให้การเขียนโปรแกรมใช้งานง่าย ทั้งคู่มีการทำงานร่วมกันของ Java ที่ดีมากดังนั้นคุณสามารถผสมผสานและจับคู่ภาษาได้ ในความเป็นจริงสำหรับการทำงานร่วมกันของ Java, Scala รวบรวมฟังก์ชั่นชั้นหนึ่งของคลาสที่ไม่ระบุชื่อที่เกือบเข้ากันได้กับอินเตอร์เฟสการทำงานของ Java 8 คุณสามารถอ่านรายละเอียดในScala ในส่วนลึก 1.3.2