วิธีเขียนโปรแกรม Java ที่มีประโยชน์โดยไม่ใช้ตัวแปรที่ไม่แน่นอน


12

ฉันอ่านบทความเกี่ยวกับการเขียนโปรแกรมการทำงานที่ผู้เขียนระบุ

(take 25 (squares-of (integers)))

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

เป็นไปได้ไหมที่จะบรรลุสิ่งนี้ใน Java? สมมติว่าคุณจำเป็นต้องพิมพ์สแควร์ของจำนวนเต็ม 15 ตัวแรกคุณสามารถเขียน a for หรือ while loop โดยไม่ใช้ตัวแปรได้หรือไม่?

แจ้งให้ทราบล่วงหน้า mod

คำถามนี้ไม่ใช่การแข่งขันกอล์ฟรหัส เรากำลังมองหาคำตอบที่อธิบายแนวคิดที่เกี่ยวข้อง (นึกคิดโดยไม่ต้องทำซ้ำคำตอบก่อนหน้า) และไม่เพียง แต่สำหรับรหัสอื่น


19
ตัวอย่างเช่นการทำงานของคุณไม่ตัวแปรใช้ในภายใน แต่ภาษาที่ไม่ทั้งหมดของมันอยู่เบื้องหลัง คุณได้มอบหมายส่วนที่ไม่พึงประสงค์ให้กับคนที่คุณเชื่อว่าทำอย่างถูกต้องแล้ว
Blrfl

12
@Blrfl: อาร์กิวเมนต์ "เบื้องหลัง" ฆ่าการอภิปรายตามภาษาทั้งหมดเนื่องจากรหัสทุกชิ้นจะถูกแปลเป็นรหัสเครื่อง x86 ในที่สุด รหัส x86 ไม่ใช่แบบเชิงวัตถุไม่ใช่แบบขั้นตอนไม่ทำงานไม่ได้มีอะไร แต่หมวดหมู่เหล่านี้เป็นแท็กที่มีค่าสำหรับภาษาการเขียนโปรแกรม ดูภาษาไม่ใช่การใช้งาน
thiton

10
@thiton ไม่เห็นด้วย อะไร Blrfl จะพูดคือว่าฟังก์ชั่นเหล่านั้นอาจจะใช้ตัวแปรในการเขียนโปรแกรมภาษาเดียวกัน ไม่จำเป็นต้องไปที่ระดับต่ำที่นี่ ตัวอย่างเป็นเพียงการใช้ฟังก์ชั่นห้องสมุด คุณสามารถเขียนโค้ดเดียวกันใน Java ได้อย่างง่ายดายโปรดดู: squaresOf(integers()).take(25)(การเขียนฟังก์ชั่นเหล่านั้นถูกทิ้งไว้เป็นแบบฝึกหัดสำหรับผู้อ่านความยากอยู่ที่การตั้งค่าอนันต์integers()แต่นั่นเป็นปัญหาสำหรับ Java เนื่องจากการประเมินผลที่กระตือรือร้น ตัวแปร)
Andres F.

6
อ้างว่าจะทำให้เกิดความสับสนและเข้าใจผิดมีเวทมนตร์ไม่มีเพียงน้ำตาลประโยค
yannis

2
@thiton ฉันขอแนะนำให้คุณเรียนรู้เพิ่มเติมเกี่ยวกับภาษา FP แต่อย่างไรก็ตามโค้ดไม่สนับสนุน (หรือปฏิเสธ) การยืนยันว่าไม่จำเป็นต้องใช้ "ตัวแปร" (ซึ่งฉันถือว่าคุณหมายถึง "ตัวแปรที่ไม่แน่นอน" เนื่องจากคนอื่น ๆ ชนิดเป็นเรื่องธรรมดาใน FP) ตัวอย่างแสดงให้เห็นถึงฟังก์ชั่นห้องสมุดที่สามารถนำมาใช้ใน Java เช่นกันยกเว้นปัญหาขี้เกียจ / กระตือรือร้นที่ offtopic ที่นี่
Andres F.

คำตอบ:


31

เป็นไปได้หรือไม่ที่จะใช้ตัวอย่างเช่นใน Java โดยไม่ใช้การอัพเดทแบบทำลายล้าง ใช่. อย่างไรก็ตามตามที่ @Thiton และบทความที่กล่าวถึงมันจะน่าเกลียด (ขึ้นอยู่กับรสนิยม) วิธีหนึ่งคือใช้การเรียกซ้ำ นี่เป็นตัวอย่างของHaskellที่ทำสิ่งที่คล้ายกัน:

unfoldr      :: (b -> Maybe (a, b)) -> b -> [a]
unfoldr f b  =
  case f b of
   Just (a,new_b) -> a : unfoldr f new_b
   Nothing        -> []  

หมายเหตุ 1) การขาดการกลายพันธุ์ 2) การใช้การเรียกซ้ำและ 3) การขาดการวนซ้ำ จุดสุดท้ายมีความสำคัญมาก - ภาษาที่ใช้งานได้ไม่จำเป็นต้องมีโครงสร้างวนลูปที่สร้างขึ้นในภาษา ต่อไปนี้เป็นเอกสารชุดที่รู้จักกันดีซึ่งแสดงว่าการเรียกใช้ฟังก์ชันที่แสดงออกอย่างไม่น่าเชื่อนั้นเป็นอย่างไร


ฉันพบบทความที่ไม่พอใจและต้องการทำคะแนนเพิ่มเติมสองสามประการ:

บทความนั้นเป็นคำอธิบายที่ไม่ดีและสร้างความสับสนให้กับการเขียนโปรแกรมเชิงหน้าที่และประโยชน์ของมัน ฉันขอแนะนำแหล่งข้อมูลอื่น ๆ สำหรับการเรียนรู้เกี่ยวกับการเขียนโปรแกรมใช้งานได้

ส่วนที่สับสนมากที่สุดเกี่ยวกับบทความนี้คือมันไม่ได้กล่าวถึงว่ามีการใช้สองคำสั่งสำหรับการมอบหมายใน Java (และภาษากระแสหลักอื่น ๆ ):

  1. ผูกค่ากับชื่อ: final int MAX_SIZE = 100;

  2. การอัพเดทแบบทำลายล้าง: int a = 3; a += 1; a++;

หลีกเลี่ยงการเขียนโปรแกรมการทำงานสอง แต่โอบกอดแรก(ตัวอย่าง: let-expressions พารามิเตอร์ฟังก์ชั่นระดับบนสุดdefine itions) นี้เป็นจุดสำคัญมากที่จะเข้าใจเพราะมิฉะนั้นบทความก็ดูเหมือนว่าโง่และอาจทำให้คุณสงสัยว่าสิ่งที่เป็นtake, squares-ofและintegersหากไม่ได้ตัวแปร?

นอกจากนี้ตัวอย่างนั้นไม่มีความหมาย มันไม่ได้แสดงการใช้งานของtake, หรือsquares-of integersสำหรับทุกสิ่งที่เรารู้พวกเขาจะดำเนินการโดยใช้ตัวแปรที่ไม่แน่นอน ตามที่ @Martin กล่าวว่าคุณสามารถเขียนตัวอย่างนี้ใน Java ได้เล็กน้อย

อีกครั้งฉันขอแนะนำให้หลีกเลี่ยงบทความนี้และคนอื่น ๆ ชอบถ้าคุณต้องการเรียนรู้เกี่ยวกับการเขียนโปรแกรมการทำงาน ดูเหมือนว่ามันจะถูกเขียนขึ้นโดยมีเป้าหมายของการตกตะลึงและไม่พอใจมากกว่าการสอนแนวคิดและปัจจัยพื้นฐาน แต่ทำไมไม่ลองดูเอกสารที่ฉันโปรดปรานตลอดกาลโดย John Hughes ฮิวจ์พยายามที่จะจัดการกับปัญหาเดียวกันกับที่กล่าวถึงในบทความ (แม้ว่าฮิวจ์ไม่ได้พูดเกี่ยวกับการเกิดพร้อมกัน / การขนาน) นี่คือทีเซอร์:

บทความนี้เป็นความพยายามที่จะแสดงให้เห็นถึงชุมชนขนาดใหญ่ของโปรแกรมเมอร์ (ไม่ใช่ฟังก์ชั่น) ความหมายของการเขียนโปรแกรมการทำงานและยังช่วยให้โปรแกรมเมอร์ทำงานใช้ประโยชน์จากข้อได้เปรียบอย่างเต็มที่โดยทำให้ชัดเจนว่าข้อดีเหล่านั้นคืออะไร

[ ... ]

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


10
+1 สำหรับ "ฉันขอแนะนำให้หลีกเลี่ยงบทความนี้และคนอื่น ๆ ชอบถ้าคุณต้องการเรียนรู้เกี่ยวกับการเขียนโปรแกรมใช้งานได้จริง ๆ แล้วดูเหมือนว่ามันจะถูกเขียนขึ้นด้วยเป้าหมายที่น่าตกใจและไม่พอใจมากกว่าการสอนแนวคิดและพื้นฐาน"

3
ครึ่งเหตุผลที่ผู้คนไม่ทำ FP ก็เพราะพวกเขาไม่ได้ยิน / เรียนรู้อะไรเกี่ยวกับเรื่องนี้ในมหาวิทยาลัยและอีกครึ่งหนึ่งเป็นเพราะเมื่อพวกเขามองเข้าไปก็พบบทความที่ทำให้พวกเขาทั้งไม่รู้และคิดว่ามันเป็นเรื่องเพ้อฝัน เล่นเกี่ยวกับแทนที่จะเป็นวิธีการที่คิดออกด้วยเหตุผลที่มีประโยชน์ +1 สำหรับการให้แหล่งข้อมูลที่ดีกว่า
Jimmy Hoffa

3
ใส่คำตอบของคำถามที่ด้านบนแน่นอนถ้าคุณจะตรงกับคำถามมากขึ้นและบางทีคำถามนี้จะเปิดอยู่ (ด้วยคำตอบที่เน้นคำถามโดยตรง)
จิมมี่ฮอฟฟา

2
ขออภัยที่ nitpick แต่ฉันไม่เข้าใจว่าทำไมคุณเลือกรหัส haskell นี้ ฉันอ่านLYAHแล้วและตัวอย่างของคุณนั้นยากสำหรับฉันที่จะอ่าน ฉันไม่เห็นความเกี่ยวข้องกับคำถามเดิม ทำไมคุณไม่ใช้take 25 (map (^2) [1..])เป็นตัวอย่าง?
Daniel Kaplan

2
@tietyT คำถามที่ดี - ขอบคุณที่ชี้ให้เห็น เหตุผลที่ฉันใช้ตัวอย่างนั้นเพราะมันแสดงวิธีสร้างรายการตัวเลขโดยใช้การเรียกซ้ำและหลีกเลี่ยงตัวแปรที่ไม่แน่นอน ความตั้งใจของฉันคือการให้ OP เห็นรหัสนั้นและคิดเกี่ยวกับวิธีการทำสิ่งที่คล้ายกันใน Java หากต้องการระบุข้อมูลโค้ดของคุณคือ[1..]อะไร นั่นเป็นคุณสมบัติเจ๋ง ๆ ที่สร้างขึ้นใน Haskell แต่ไม่ได้แสดงแนวคิดที่อยู่เบื้องหลังการสร้างรายการดังกล่าว ฉันแน่ใจว่าอินสแตนซ์ของEnumคลาส (ซึ่งไวยากรณ์นั้นต้องการ) ก็มีประโยชน์เช่นกัน แต่ก็ขี้เกียจเกินไปที่จะค้นหา ดังนั้นunfoldr. :)

27

คุณจะไม่ ตัวแปรเป็นหัวใจสำคัญของการเขียนโปรแกรมที่จำเป็นและถ้าคุณพยายามที่จะเขียนโปรแกรมอย่างไม่จำเป็นโดยไม่ต้องใช้ตัวแปรคุณก็แค่ทำให้ทุกคนเจ็บปวดในตูด ในกระบวนทัศน์การเขียนโปรแกรมที่แตกต่างกันสไตล์จะแตกต่างกันและแนวคิดที่แตกต่างกันเป็นพื้นฐานของคุณ ตัวแปรใน Java เมื่อใช้งานได้ดีกับขอบเขตขนาดเล็กจะไม่ชั่วร้าย การขอโปรแกรม Java ที่ไม่มีตัวแปรนั้นก็เหมือนกับการขอโปรแกรม Haskell ที่ไม่มีฟังก์ชั่นดังนั้นคุณไม่ต้องขอมันและคุณจะไม่ถูกหลอกให้เข้าไปดูโปรแกรมที่จำเป็นเพราะมันใช้ตัวแปร

ดังนั้นวิธี Java จะเป็น:

for (int i = 1; i <= 25; ++i) {
    System.out.println(i*i);
}

และอย่าปล่อยให้ตัวเองถูกหลอกให้เขียนมันในลักษณะที่ซับซ้อนมากขึ้นเนื่องจากความเกลียดชังของตัวแปร


5
"ความเกลียดชังของตัวแปร"? Ooookay ... คุณอ่านเกี่ยวกับฟังก์ชั่นการเขียนโปรแกรมอะไรบ้าง? คุณลองภาษาอะไร บทเรียนใด
Andres F.

8
@AndresF: หลักสูตรมากกว่าสองปีใน Haskell ฉันไม่ได้บอกว่า FP แย่ อย่างไรก็ตามมีแนวโน้มในการสนทนา FP-vs-IP จำนวนมาก (เช่นบทความที่เชื่อมโยง) เพื่อประณามการใช้เอนทิตีที่กำหนดชื่อได้อีกครั้ง (ตัวแปร AKA) และประณามโดยไม่มีเหตุผลหรือข้อมูลที่ดี การลงโทษที่ไม่สมควรมีความเกลียดชังในหนังสือของฉัน และความเกลียดชังทำให้รหัสไม่ดีจริงๆ
thiton

10
"ความเกลียดชังของตัวแปร" เป็นสาเหตุที่ทำให้เกิดการขยายตัวเกินเหตุen.wikipedia.org/wiki/Fallacy_of_the_single_causeมีประโยชน์มากมายสำหรับการเขียนโปรแกรมไร้สัญชาติซึ่งอาจมีอยู่ใน Java แม้ว่าฉันจะเห็นด้วยกับคำตอบของคุณว่าใน Java ค่าใช้จ่ายจะสูงเกินไปในความซับซ้อน โปรแกรมและไม่เป็นสำนวน ฉันยังคงไม่ยอมปัดความคิดออกไปว่าการเขียนโปรแกรมไร้รัฐนั้นดีและไม่ดีเพราะการตอบสนองทางอารมณ์มากกว่าการตอบสนองที่มีเหตุผล
จิมมี่ฮอฟฟา

2
สอดคล้องกับสิ่งที่ @JimmyHoffa พูดว่าฉันจะแนะนำคุณไปที่ John Carmack ในหัวข้อการเขียนโปรแกรมสไตล์การใช้งานในภาษาที่จำเป็น (C ++ ในกรณีของเขา) ( altdevblogaday.com/2012/04/26/functional-programming-in-c )
Steven Evers

5
การกล่าวโทษที่ไม่สมเหตุผลไม่ได้เป็นความเกลียดชังและการหลีกเลี่ยงสภาวะที่ไม่แน่นอนจะไม่สมเหตุสมผล
Michael Shaw

21

วิธีที่ง่ายที่สุดที่ฉันสามารถทำได้ด้วยการเรียกซ้ำคือฟังก์ชันที่มีพารามิเตอร์เดียว มันไม่ได้เป็น Java-esque แต่มันได้ผล:

public class squares
{
    public static void main(String[] args)
    {
        squares(15);
    }

    private static void squares(int x)
    {
        if (x>0)
        {
            System.out.println(x*x);
            squares(x-1);
        }
    }
}

3
+1 สำหรับการพยายามตอบคำถามด้วยตัวอย่าง Java
KChaloux

ฉันลงคะแนนนี้สำหรับการนำเสนอสไตล์การเล่นกอล์ฟ (ดูประกาศเกี่ยวกับ Mod ) แต่ไม่สามารถบังคับตัวเองให้กดลูกศรลงได้เพราะรหัสนี้ตรงกับข้อความในคำตอบที่ฉันชอบ : "1) การขาดการกลายพันธุ์ 2) การใช้ การเรียกซ้ำและ 3) การขาดลูป "
gnat

3
@gnat: คำตอบนี้ถูกโพสต์ก่อนแจ้งให้ทราบ Mod ฉันไม่ได้เป็นคนที่มีสไตล์ที่ยอดเยี่ยมฉันกำลังทำเรื่องง่าย ๆ และตอบสนองคำถามดั้งเดิมของ OP; เพื่อแสดงให้เห็นว่าเป็นไปได้ที่จะทำสิ่งต่าง ๆ ใน Java
FrustratedWithFormsDesigner

@FrustratedWithFormsDesigner แน่นอน; สิ่งนี้จะไม่หยุดฉันจาก DVing (เนื่องจากคุณควรมีความสามารถในการแก้ไขการปฏิบัติตาม) - มันเป็นคู่ที่สมบูรณ์แบบที่ยอดเยี่ยมที่ทำเวทย์มนตร์ ทำได้ดีมากทำได้ดีมีความรู้ดีมากขอบคุณ
gnat

16

ในตัวอย่างการทำงานของคุณเราไม่เห็นวิธีการใช้งานsquares-ofและtakeฟังก์ชั่น ฉันไม่ใช่ผู้เชี่ยวชาญเกี่ยวกับ Java แต่ฉันค่อนข้างมั่นใจว่าเราสามารถเขียนฟังก์ชันเหล่านั้นเพื่อเปิดใช้งานคำสั่งเช่นนี้ ...

squares_of(integers).take(25);

ซึ่งไม่แตกต่างกันมาก


6
Nitpick: squares-ofไม่ใช่ชื่อที่ถูกต้องใน Java ( squares_ofคือแม้ว่า) แต่อย่างอื่นจุดที่ดีซึ่งแสดงให้เห็นว่าตัวอย่างของบทความไม่ดี

ฉันสงสัยว่าบทความที่integerเฉื่อยชาสร้างจำนวนเต็มและtakeฟังก์ชั่นหยิบ 25 ของตัวเลขจากsquared-of integerในระยะสั้นคุณควรมีintegerฟังก์ชั่นที่จะผลิตจำนวนเต็มเป็นจำนวนอนันต์
OnesimusUnbound

มันเป็นความวิกลจริตเล็กน้อยที่จะเรียกบางสิ่งบางอย่างเช่น(integer)ฟังก์ชั่น - ฟังก์ชั่นยังคงเป็นสิ่งที่แมปอาร์กิวเมนต์กับค่า ปรากฎว่า(integer)ไม่ใช่ฟังก์ชั่น แต่เป็นเพียงคุณค่า ใคร ๆ ก็สามารถพูดได้ว่าintegerเป็นตัวแปรที่ถูกผูกไว้กับตัวเลขที่ไม่สิ้นสุด
Ingo

6

ใน Java คุณสามารถทำได้ (โดยเฉพาะส่วนที่ไม่มีที่สิ้นสุด) กับตัววนซ้ำ ในตัวอย่างโค้ดต่อไปนี้จำนวนที่กำหนดให้กับตัวTakeสร้างสามารถสูงได้โดยพลการ

class Example {
    public static void main(String[] a) {
        Numbers test = new Take(25, new SquaresOf(new Integers()));
        while (test.hasNext())
            System.out.println(test.next());
    }
}

หรือด้วยวิธีโรงงาน chainable:

class Example {
    public static void main(String[] a) {
        Numbers test = Numbers.integers().squares().take(23);
        while (test.hasNext())
            System.out.println(test.next());
    }
}

ที่ไหนSquaresOf, TakeและIntegersขยายNumbers

abstract class Numbers implements Iterator<Integer> {
    public static Numbers integers() {
        return new Integers();
    }

    public Numbers squares() {
        return new SquaresOf(this);
    }

    public Numbers take(int c) {
        return new Take(c, this);
    }
    public void remove() {}
}

1
นี่แสดงให้เห็นถึงความเหนือกว่าของกระบวนทัศน์ OO เหนือสิ่งที่ใช้งานได้ ด้วยการออกแบบ OO ที่เหมาะสมคุณสามารถเลียนแบบกระบวนทัศน์การทำงาน แต่คุณไม่สามารถเลียนแบบกระบวนทัศน์ OO ในสไตล์การใช้งานได้
m3th0dman

3
@ m3th0dman: ด้วยการออกแบบ OO ที่เหมาะสมคุณสามารถเลียนแบบ FP ครึ่งเหมือนภาษาใด ๆ ที่มีสตริงรายชื่อและ / หรือพจนานุกรมสามารถเลียนแบบ OO ครึ่งทาง ความเท่าเทียมกันของทัวริงของภาษาที่ใช้งานทั่วไปหมายความว่าได้รับความพยายามเพียงพอภาษาใดก็ตามสามารถจำลองคุณสมบัติของอื่น ๆ ได้
cHao

โปรดทราบว่าตัววนซ้ำ Java แบบในwhile (test.hasNext()) System.out.println(test.next())จะเป็น no-no ใน FP; ตัววนซ้ำจะไม่แน่นอน
cHao

1
@ cHao ฉันแทบจะไม่เชื่อว่า encapsulation หรือ polymorphism ที่แท้จริงสามารถลอกเลียนแบบได้ Java (ในตัวอย่างนี้) ไม่สามารถเลียนแบบภาษาที่ใช้งานได้จริงเนื่องจากการประเมินที่กระตือรือร้น ฉันยังเชื่อว่าตัววนซ้ำสามารถเขียนในลักษณะวนซ้ำได้
m3th0dman

@ m3th0dman: ความแตกต่างจะไม่ยากเลยที่จะเลียนแบบ; แม้แต่ภาษาซีและภาษาแอสเซมบลีก็สามารถทำได้ เพียงแค่ทำให้วิธีการเป็นเขตข้อมูลในวัตถุหรือ class descriptor / vtable และการห่อหุ้มข้อมูลไม่จำเป็นต้องใช้ความรู้สึกในการซ่อนข้อมูล ครึ่งภาษาที่อยู่ในนั้นไม่มีให้เมื่อวัตถุของคุณไม่เปลี่ยนรูปมันไม่สำคัญเลยว่าผู้คนจะเห็นความกล้าของมันอยู่ดี สิ่งที่จำเป็นต้องมีคือการห่อข้อมูลซึ่งวิธีการดังกล่าวข้างต้นสามารถให้ได้อย่างง่ายดาย
cHao

6

เวอร์ชั่นสั้น:

ในการที่จะทำให้รูปแบบการมอบหมายเดี่ยวทำงานได้อย่างน่าเชื่อถือใน Java คุณจะต้อง (1) โครงสร้างพื้นฐานที่ไม่เปลี่ยนรูปได้ง่ายและ (2) คอมไพเลอร์ - หรือการสนับสนุนระดับรันไทม์สำหรับการกำจัดการโทรหาง

เราสามารถเขียนโครงสร้างพื้นฐานได้มากมายและเราสามารถจัดเรียงสิ่งต่าง ๆ เพื่อพยายามหลีกเลี่ยงการเติมสแต็ค แต่ตราบใดที่การโทรแต่ละครั้งใช้กรอบสแต็กจะมีการ จำกัด จำนวนการเรียกซ้ำที่คุณสามารถทำได้ ทำให้ iterables เล็กและ / หรือขี้เกียจและคุณไม่ควรมีปัญหาสำคัญ อย่างน้อยที่สุดปัญหาที่คุณพบไม่จำเป็นต้องส่งคืนผลลัพธ์ล้านรายการในครั้งเดียว :)

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


รุ่นยาว:

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

Java ได้รับการออกแบบตั้งแต่เริ่มต้นเป็นความจำเป็น , เชิงวัตถุภาษา

  • ภาษาที่มีความหมายมักขึ้นอยู่กับตัวแปรที่ไม่แน่นอนบางชนิด พวกเขามีแนวโน้มที่จะสนับสนุนการทำซ้ำมากกว่าการเรียกซ้ำตัวอย่างเช่นและการสร้างซ้ำเกือบทั้งหมด - แม้while (true)และfor (;;)! - ขึ้นอยู่กับตัวแปรอย่างเต็มที่บางที่เปลี่ยนจากการทำซ้ำเป็นซ้ำ
  • ภาษาเชิงวัตถุจะมองเห็นทุกโปรแกรมเป็นกราฟของวัตถุที่ส่งข้อความถึงกันและในเกือบทุกกรณีตอบสนองต่อข้อความเหล่านั้นโดยการปิดบังบางสิ่ง

ผลลัพธ์สุดท้ายของการตัดสินใจออกแบบคือไม่มีตัวแปรที่ไม่สามารถเปลี่ยนแปลงได้ Java ไม่มีทางที่จะเปลี่ยนสถานะของสิ่งใด - แม้แต่สิ่งที่เรียบง่ายเหมือนกับการพิมพ์ "Hello world!" ไปที่หน้าจอเกี่ยวข้องกับกระแสข้อมูลขาออกซึ่งเกี่ยวข้องกับการผสานไบต์ในบัฟเฟอร์ที่ไม่แน่นอน

ดังนั้นเพื่อวัตถุประสงค์เชิงปฏิบัติทั้งหมดเราถูก จำกัด ให้กำจัดตัวแปรจากรหัสของเราเอง ตกลงเราสามารถทำได้ เกือบจะ โดยพื้นฐานสิ่งที่เราต้องการคือแทนที่การวนซ้ำเกือบทั้งหมดด้วยการเรียกซ้ำและการกลายพันธุ์ทั้งหมดด้วยการเรียกซ้ำเรียกคืนค่าที่เปลี่ยนแปลง อย่างนั้น ...

class Ints {
     final int value;
     final Ints tail;

     public Ints(int value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints next() { return this.tail; }
     public int value() { return this.value; }
}

public Ints take(int count, Ints input) {
    if (count == 0 || input == null) return null;
    return new Ints(input.value(), take(count - 1, input.next()));
}    

public Ints squares_of(Ints input) {
    if (input == null) return null;
    int i = input.value();
    return new Ints(i * i, squares_of(input.next()));
}

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

ตอนนี้เป็นรุ่นที่มีการดัดแปลงอย่างมากของสิ่งนี้ แต่มันก็ดีพอที่จะแสดงให้เห็นถึงปัญหาร้ายแรงด้วยวิธีนี้ใน Java พิจารณารหัสนี้:

public function doStuff() {
    final Ints integers = ...somehow assemble list of 20 million ints...;
    final Ints result = take(25, squares_of(integers));
    ...
}

แม้ว่าเราต้องการ 25 ints สำหรับผลลัพธ์ แต่squares_ofก็ไม่ทราบ มันจะคืนค่ากำลังสองของจำนวนทุกintegersค่า การเรียกซ้ำ 20 ล้านระดับทำให้เกิดปัญหาใหญ่ใน Java

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

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

หนึ่งวิธีแก้ปัญหา ... การประเมินผลที่ขี้เกียจ เรายังคงมีข้อ จำกัด ของสแต็ก แต่สามารถเชื่อมโยงกับปัจจัยที่เราควบคุมได้มากกว่า เราไม่ต้องคำนวณ ints หนึ่งล้านแค่คืน 25 :)

ดังนั้นเรามาสร้างโครงสร้างพื้นฐานการประเมินผลที่ขี้เกียจกับเรา (รหัสนี้ได้รับการทดสอบในขณะที่กลับ แต่ฉันได้แก้ไขมันค่อนข้างน้อยแล้วอ่านความคิดไม่ใช่ข้อผิดพลาดทางไวยากรณ์ :))

// Represents something that can give us instances of OutType.
// We can basically treat this class like a list.
interface Source<OutType> {
     public Source<OutType> next();
     public OutType value();
}

// Represents an operation that turns an InType into an OutType.
// Note, these can be the same type.  We're just flexible like that.
interface Transform<InType, OutType> {
    public OutType appliedTo(InType input);
}

// Represents an action (as opposed to a function) that can run on
// every element of a sequence.
abstract class Action<InType> {
    abstract void doWith(final InType input);
    public void doWithEach(final Source<InType> input) {
        if (input == null) return;
        doWith(input.value());
        doWithEach(input.next());
    }
}

// A list of Integers.
class Ints implements Source<Integer> {
     final Integer value;
     final Ints tail;
     public Ints(Integer value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints(Source<Integer> input) {
         this.value = input.value();
         this.tail = new Ints(input.next());
     }
     public Source<Integer> next() { return this.tail; }
     public Integer value() { return this.value; }
     public static Ints fromArray(Integer[] input) {
         return fromArray(input, 0, input.length);
     }
     public static Ints fromArray(Integer[] input, int start, int end) {
         if (end == start || input == null) return null;
         return new Ints(input[start], fromArray(input, start + 1, end));
     }
}

// An example of the spiff we get by splitting the "iterator" interface
// off.  These ints are effectively generated on the fly, as opposed to
// us having to build a huge list.  This saves huge amounts of memory
// and CPU time, for the rather common case where the whole sequence
// isn't needed.
class Range implements Source<Integer> {
    final int start, end;
    public Range(int start, int end) {
        this.start = start;
        this.end = end;
    }
    public Integer value() { return start; }
    public Source<Integer> next() {
        if (start >= end) return null;
        return new Range(start + 1, end);
    }
}

// This takes each InType of a sequence and turns it into an OutType.
// This *takes* a Transform, rather than just *implementing* Transform,
// because the transforms applied are likely to be specified inline.
// If we just let people override `value()`, we wouldn't easily know what type
// to return, and returning our own type would lose the transform method.
static class Mapper<InType, OutType> implements Source<OutType> {
    private final Source<InType> input;
    private final Transform<InType, OutType> transform;

    public Mapper(Transform<InType, OutType> transform, Source<InType> input) {
        this.transform = transform;
        this.input = input;
    }

    public Source<OutType> next() {
         return new Mapper<InType, OutType>(transform, input.next());
    }
    public OutType value() {
         return transform.appliedTo(input.value());
    }
}

// ...

public <T> Source<T> take(int count, Source<T> input) {
    if (count <= 0 || input == null) return null;
    return new Source<T>() {
        public T value() { return input.value(); }
        public Source<T> next() { return take(count - 1, input.next()); }
    };
}

(โปรดทราบว่าหากสิ่งนี้สามารถใช้งานได้จริงใน Java โค้ดอย่างน้อยค่อนข้างคล้ายกับที่กล่าวมาจะเป็นส่วนหนึ่งของ API)

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

public Source<Integer> squares_of(Source<Integer> input) {
     final Transform<Integer, Integer> square = new Transform<Integer, Integer>() {
         public Integer appliedTo(final Integer i) { return i * i; }
     };
     return new Mapper<>(square, input);
}


public void example() {
    final Source<Integer> integers = new Range(0, 1000000000);

    // and, as for the author's "bet you can't do this"...
    final Source<Integer> squares = take(25, squares_of(integers));

    // Just to make sure we got it right :P
    final Action<Integer> printAction = new Action<Integer>() {
        public void doWith(Integer input) { System.out.println(input); }
    };
    printAction.doWithEach(squares);
}

ส่วนใหญ่ใช้งานได้ แต่ก็ยังค่อนข้างมีแนวโน้มที่จะสแต็คล้น ลองใช้takeints 2 พันล้านและดำเนินการกับมัน : P ในที่สุดมันจะส่งข้อยกเว้นอย่างน้อยก็จนกว่า RAM 64+ GB จะกลายเป็นมาตรฐาน ปัญหาคือจำนวนหน่วยความจำของโปรแกรมที่สำรองไว้สำหรับสแต็กของมันไม่ใหญ่มาก โดยทั่วไปแล้วจะอยู่ระหว่าง 1 ถึง 8 MiB (คุณสามารถขอที่ใหญ่กว่าได้ แต่มันไม่สำคัญว่าคุณจะขอเท่าไหร่ - คุณโทรหาtake(1000000000, someInfiniteSequence)คุณจะได้รับการยกเว้น) โชคดีที่มีการประเมินที่ขี้เกียจจุดอ่อนอยู่ในพื้นที่ที่เราสามารถควบคุมได้ดีกว่า . take()เราก็จะต้องระมัดระวังเกี่ยวกับเราเท่าไหร่

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

public <T> void doSomethingWith(T input) { /* magic happens here */ }
public <T> Source<T> workWith(Source<T> input, int count) {
    if (count < 0 || input == null) return null;
    if (count == 0) return input;
    if (count == 1) {
        doSomethingWith(input.value());
        return input.next();
    }
    return (workWith(workWith(input, count/2), count - count/2);
}

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

ปัญหาคือฟังก์ชั่นนี้ต้องการอินพุต - และด้วยรายการที่เชื่อมโยงการรับความยาวต้องผ่านรายการทั้งหมด ที่แก้ไขได้อย่างง่ายดายแม้ว่า; เพียงแค่ไม่สนใจวิธีการหลายรายการที่มี :) รหัสข้างต้นจะทำงานกับสิ่งที่ชอบInteger.MAX_VALUEนับเป็นโมฆะหยุดการประมวลผลต่อไป การนับนั้นส่วนใหญ่มีดังนั้นเราจึงมีตัวฐานที่แน่นหนา หากคุณคาดว่าจะมีมากกว่าInteger.MAX_VALUEรายการในรายการคุณสามารถตรวจสอบworkWithค่าส่งคืนของ - มันควรจะเป็นโมฆะในตอนท้าย มิฉะนั้นเรียกเก็บเงิน

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


3

ก่อนหน้านี้ฉันได้พยายามสร้างล่ามสำหรับภาษาที่เหมือนกระเพื่อมใน Java (ไม่กี่ปีที่ผ่านมาและรหัสทั้งหมดหายไปเหมือนใน CVS ที่ sourceforge) และจาวาใช้ iterators เป็น verbose เล็กน้อยสำหรับการเขียนโปรแกรมการทำงาน ในรายการ

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

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

เห็นได้ชัดว่าintegersควรเป็นรายการจำนวนเต็มทั้งหมดดังนั้นฉันเริ่มที่ศูนย์และส่งคืนค่าบวกและค่าลบสลับกัน

มีสองเวอร์ชั่นของสี่เหลี่ยมจัตุรัส - อันหนึ่งสร้างลำดับที่กำหนดเองส่วนอีกอันใช้mapซึ่งใช้ 'ฟังก์ชั่น' - Java 7 ไม่มี lambdas ดังนั้นฉันจึงใช้อินเทอร์เฟซ - และใช้กับแต่ละองค์ประกอบตามลำดับ

จุดของsquare ( int x )ฟังก์ชั่นนี้มีเพียงการลบความต้องการที่จะเรียกhead()สองครั้ง - โดยปกติฉันจะทำสิ่งนี้โดยใส่ค่าลงในตัวแปรสุดท้าย แต่การเพิ่มฟังก์ชั่นนี้หมายความว่าไม่มีตัวแปรในโปรแกรมเพียงพารามิเตอร์ฟังก์ชันเท่านั้น

ความละเอียดของ Java สำหรับการเขียนโปรแกรมประเภทนี้ทำให้ฉันเขียนล่ามรุ่นที่สองใน C99 แทน

public class Squares {
    interface Seq<T> {
        T head();
        Seq<T> tail();
    }

    public static void main (String...args) {
        print ( take (25, integers ) );
        print ( take (25, squaresOf ( integers ) ) );
        print ( take (25, squaresOfUsingMap ( integers ) ) );
    }

    static Seq<Integer> CreateIntSeq ( final int n) {
        return new Seq<Integer> () {
            public Integer head () {
                return n;
            }
            public Seq<Integer> tail () {
                return n > 0 ? CreateIntSeq ( -n ) : CreateIntSeq ( 1 - n );
            }
        };
    }

    public static final Seq<Integer> integers = CreateIntSeq(0);

    public static Seq<Integer> squaresOf ( final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return square ( source.head() );
            }
            public Seq<Integer> tail () {
                return squaresOf ( source.tail() );
            }
        };
    }

    // mapping a function over a list rather than implementing squaring of each element
    interface Fun<T> {
        T apply ( T value );
    }

    public static Seq<Integer> squaresOfUsingMap ( final Seq<Integer> source ) {
        return map ( new Fun<Integer> () {
            public Integer apply ( final Integer value ) {
                return square ( value );
            }
        }, source );
    }

    public static <T> Seq<T> map ( final Fun<T> fun, final Seq<T> source ) {
        return new Seq<T> () {
            public T head () {
                return fun.apply ( source.head() );
            }
            public Seq<T> tail () {
                return map ( fun, source.tail() );
            }
        };
    }

    public static Seq<Integer> take ( final int count,  final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return source.head();
            }
            public Seq<Integer> tail () {
                return count > 0 ? take ( count - 1, source.tail() ) : nil;
            }
        };
    }

    public static int square ( final int x ) {
        return x * x;
    }

    public static final Seq<Integer> nil = new Seq<Integer> () {
        public Integer head () {
            throw new RuntimeException();
        }
        public Seq<Integer> tail () {
            return this;
        }
    };

    public static <T> void print ( final Seq<T> seq ) {
        printPartSeq ( "[", seq.head(), seq.tail() );
    }

    private static <T> void printPartSeq ( final String prefix, final T value, final Seq<T> seq ) {
        if ( seq == nil) {
            System.out.println("]");
        } else {
            System.out.print(prefix);
            System.out.print(value);
            printPartSeq ( ",", seq.head(), seq.tail() );
        }
    }
}

3

วิธีเขียนโปรแกรม Java ที่มีประโยชน์โดยไม่ใช้ตัวแปรที่ไม่แน่นอน

ในทางทฤษฎีคุณสามารถใช้อะไรก็ได้ใน Java โดยใช้การเรียกซ้ำเพียงอย่างเดียวและไม่มีตัวแปรที่ไม่แน่นอน

ในทางปฏิบัติ:

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

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

  • การใช้งาน Java กระแสหลักไม่สนับสนุนการเพิ่มประสิทธิภาพการโทรหาง นั่นหมายความว่าอัลกอริธึมเวอร์ชันแบบเรียกซ้ำมีแนวโน้มว่าจะเป็นพื้นที่ว่าง "หิว" และตั้งแต่ Java กองด้ายจะไม่เติบโตคุณจะต้อง preallocate กองใหญ่ ... StackOverflowErrorหรือความเสี่ยง

รวมสามสิ่งเหล่านี้และ Java ไม่ได้จริงๆที่ทำงานได้ตัวเลือกสำหรับการเขียนที่มีประโยชน์ (เช่นไม่น่ารำคาญ) โปรแกรมโดยไม่ต้องตัวแปรที่ไม่แน่นอน

(แต่เดี๋ยวก่อนก็โอเคมีภาษาโปรแกรมอื่น ๆ สำหรับ JVM ซึ่งบางภาษารองรับการเขียนโปรแกรมที่ใช้งานได้)


2

ขณะที่เรากำลังมองหาตัวอย่างของแนวความคิดฉันจะบอกว่าให้แยก Java และมองหาการตั้งค่าที่แตกต่าง แต่คุ้นเคยที่จะหาแนวคิดที่คุ้นเคย ท่อ UNIX ค่อนข้างคล้ายกับฟังก์ชั่นการขี้เกียจ

cat /dev/zero | tr '\0' '\n' | cat -n | awk '{ print $0 * $0 }' | head 25

ใน Linux หมายถึงนี้ให้ไบต์แก่ฉันซึ่งประกอบด้วยเท็จมากกว่าบิตจริงจนกว่าฉันจะหมดความกระหาย เปลี่ยนแต่ละไบต์เหล่านั้นเป็นอักขระขึ้นบรรทัดใหม่ จำนวนแต่ละบรรทัดจึงสร้าง; และสร้างสแควร์ของจำนวนนั้น นอกจากนี้ฉันมีความกระหาย 25 บรรทัดและไม่มาก

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

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

ในทางตรงกันข้ามฉันอ้างว่าโปรแกรมเมอร์คนต่อไปอาจยอมรับได้มากกว่านี้หากบางแพคเกจ "Java" ของเราเป็นจริงแพคเกจ Java Virtual Machine ที่เขียนด้วยภาษาฟังก์ชันหรือวัตถุ / หน้าที่การทำงานเช่น Clojure และ Scala สิ่งเหล่านี้ได้รับการออกแบบให้มีการเข้ารหัสโดยการผูกมัดฟังก์ชั่นร่วมกันและจะถูกเรียกจาก Java ในลักษณะปกติของการเรียกใช้เมธอด Java

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

เมื่อเร็ว ๆ นี้เทคนิคที่ฉันโปรดปราน [คือ] ใช้ตัวแปรส่งคืนที่ไม่เปลี่ยนแปลงและไม่ได้เตรียมการและทางออกเดียวเพื่อให้ในขณะที่คอมไพเลอร์ภาษาที่ใช้งานได้บางอย่าง Java ตรวจสอบว่าไม่ว่าจะเกิดอะไรขึ้นในเนื้อหาของฟังก์ชัน ค่าส่งคืน ตัวอย่าง:

int f(final int n) {
    final int result; // not initialized here!
    if (n < 0) {
        result = -n;
    } else if (n < 1) {
        result = 0;
    } else {
        result = n - 1;
    }
    // If I would leave off the "else" clause,
    // Java would fail to compile complaining that
    // "result" is possibly uninitialized.
    return result;
}


ฉันประมาณ 70% Java บางคนทำการตรวจสอบค่าส่งคืนแล้ว คุณควรได้รับข้อผิดพลาดเกี่ยวกับ "คำสั่งการส่งคืนที่หายไป" หากการควบคุมสามารถหลุดออกจากจุดสิ้นสุดของฟังก์ชั่นที่ไม่เป็นโมฆะ
cHao

จุดของฉัน: หากคุณเขียนมันเพราะint result = -n; if (n < 1) { result = 0 } return result;มันรวบรวมได้ดีและคอมไพเลอร์ก็ไม่รู้ว่าคุณตั้งใจจะทำให้มันเทียบเท่ากับฟังก์ชั่นในตัวอย่างของฉันหรือไม่ บางทีตัวอย่างนั้นง่ายเกินไปที่จะทำให้เทคนิคดูมีประโยชน์ แต่ในฟังก์ชั่นที่มีกิ่งก้านจำนวนมากฉันรู้สึกว่าดีที่จะให้ความกระจ่างว่าผลลัพธ์นั้นได้รับมอบหมายเพียงครั้งเดียวโดยไม่คำนึงถึงเส้นทางที่จะตามมา
minopret

ถ้าคุณบอกว่าif (n < 1) return 0; else return -n;คุณไม่มีปัญหา ... และมันก็ง่ายกว่า :) ดูเหมือนกับฉันว่าในกรณีนั้นกฎ "หนึ่งคืน" จริง ๆ แล้วช่วยทำให้ปัญหาไม่ทราบเมื่อตั้งค่าส่งคืนของคุณ ไม่เช่นนั้นคุณสามารถส่งคืนได้และ Java จะสามารถระบุได้ว่าเส้นทางอื่นอาจไม่ส่งคืนค่ามากขึ้นเพราะคุณไม่ต้องหารการคำนวณมูลค่าอีกต่อไปจากการส่งคืนจริง
cHao

หรือสำหรับตัวอย่างคำตอบของif (n < 0) return -n; else if (n == 0) return 0; else return n - 1;คุณ
cHao

ฉันเพิ่งตัดสินใจว่าฉันไม่ต้องการใช้ช่วงเวลาใด ๆ ในชีวิตของฉันเพื่อปกป้องกฎ OnlyOneReturn ใน Java ออกไป เมื่อใดและถ้าฉันคิดว่าการเขียนโค้ด Java ที่ฉันรู้สึกเหมือนการปกป้องที่ได้รับอิทธิพลจากการปฏิบัติการเขียนโปรแกรมการทำงานฉันจะใส่แทนตัวอย่างเช่น จนกว่าจะถึงตอนนั้นก็ไม่มีตัวอย่าง
minopret

0

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

module Main where

result = take 25 (map sqr [1..]) where sqr x = x*x

หลังจากสองสามวันฉันพบความคิดของฉันกลับไปที่คำตอบนี้ หลังจากส่วนหนึ่งของข้อเสนอแนะของฉันคือการใช้ส่วนการเขียนโปรแกรมการทำงานใน Scala ถ้าเราพิจารณาถึงการใช้สกาลาในจุดเหล่านั้นที่เราจริงๆมี Haskell ในใจ (และผมคิดว่าผมไม่ได้เป็นเพียงหนึ่งblog.zlemma.com/2013/02/20/... ) ไม่ควรอย่างน้อยเราพิจารณา Frege?
minopret

@minopret นี่คือช่องของ Frege ที่กำลังเดินย่ำ - ผู้คนรู้จักและรัก Haskell และยังต้องการ JVM ฉันมั่นใจวันหนึ่ง Frege จะโตพอที่จะได้รับการพิจารณาอย่างจริงจังอย่างน้อย
Ingo
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.