อะไรคือสิ่งที่เทียบเท่าการทำงานของคำสั่งทำลายความจำเป็นและการตรวจสอบวงอื่น ๆ ?


36

สมมติว่าฉันเป็นตรรกะด้านล่าง วิธีการเขียนในฟังก์ชั่นการเขียนโปรแกรม?

    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;
    }

ตัวอย่างในบล็อกบทความส่วนใหญ่ ... ฉันเห็นเพียงแค่อธิบายกรณีง่าย ๆ ของฟังก์ชันทางคณิตศาสตร์ตรงไปข้างหน้าหนึ่งคำว่า 'ผลรวม' แต่ฉันมีเหตุผลคล้ายกับที่เขียนไว้ข้างต้นใน Java และต้องการย้ายที่ไปยังรหัสการทำงานใน Clojure หากเราไม่สามารถทำตามข้างต้นใน FP ดังนั้นการส่งเสริมการขายประเภท FP ไม่ได้ระบุไว้อย่างชัดเจน

ฉันรู้ว่ารหัสข้างต้นมีความจำเป็นอย่างยิ่ง มันไม่ได้เขียนด้วยความคาดหวังของการโยกย้ายไปยัง FP ในอนาคต


1
โปรดทราบว่าการรวมกันของbreakและreturn answerสามารถถูกแทนที่ด้วยreturnวงใน ใน FP คุณสามารถใช้ผลตอบแทนก่อนกำหนดได้โดยใช้การต่อเนื่องดูที่en.wikipedia.org/wiki/Continuation
Giorgio

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

8
ในกรณีนี้: takeWhile.
Jonathan Cast

1
@ WillNess: ฉันแค่อยากจะพูดถึงมันเพราะมันสามารถใช้ในการออกจากการคำนวณที่ซับซ้อนได้ทุกจุด มันอาจไม่ใช่ทางออกที่ดีที่สุดสำหรับตัวอย่างที่เป็นรูปธรรมของ OP
Giorgio

@Giorgio คุณพูดถูกมันครอบคลุมมากที่สุดโดยทั่วไป ที่จริงคำถามนี้กว้างมาก IYKWIM (เช่นจะปิดใน SO ในการเต้นของหัวใจ)
Will Ness

คำตอบ:


45

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

doSomeCalc :: [Int] -> Int
doSomeCalc values = foldr1 combine values
  where combine v1 v2 | v1 == 10  = v1
                      | v1 == 150 = v1 + 100 + v2
                      | otherwise = v1 + v2

ทำลายสิ่งนี้ลงทีละบรรทัดในกรณีที่คุณไม่คุ้นเคยกับไวยากรณ์ของ Haskell สิ่งนี้จะทำงานดังนี้:

doSomeCalc :: [Int] -> Int

กำหนดประเภทของฟังก์ชั่นการยอมรับรายการของ int และกลับมา int เดียว

doSomeCalc values = foldr1 combine values

ตัวหลักของฟังก์ชั่นนี้ได้รับการโต้แย้งvaluesกลับมาfoldr1เรียกว่ามีข้อโต้แย้งcombine(ซึ่งเราจะกำหนดด้านล่าง) valuesและ foldr1เป็นตัวแปรของพับแบบดั้งเดิมที่เริ่มต้นด้วยชุดสะสมเพื่อค่าแรกของรายการ (เพราะฉะนั้น1ในชื่อของฟังก์ชั่น) จากนั้นรวมโดยใช้การใช้ฟังก์ชั่นที่ระบุจากซ้ายไปขวา (ซึ่งมักจะเรียกว่าพับขวา , ดังนั้นrในชื่อฟังก์ชั่น) ดังนั้นจึงfoldr1 f [1,2,3]เทียบเท่ากับf 1 (f 2 3)(หรือf(1,f(2,3))ในรูปแบบ C-like ทั่วไปมากกว่า)

  where combine v1 v2 | v1 == 10  = v1

กำหนดcombineฟังก์ชั่นท้องถิ่น: ได้รับทั้งสองมีปากเสียงและv1 v2เมื่อv110 v1ก็แค่ผลตอบแทน ในกรณีนี้v2 ไม่เคยถูกประเมินดังนั้นลูปจะหยุดที่นี่

                      | v1 == 150 = v1 + 100 + v2

อีกทางเลือกหนึ่งเมื่อ v1 คือ 150 เพิ่มอีก 100 พิเศษและเพิ่ม v2

                      | otherwise = v1 + v2

และหากเงื่อนไขเหล่านั้นไม่เป็นจริงเพียงเพิ่ม v1 ถึง v2

ตอนนี้วิธีแก้ปัญหานี้ค่อนข้างเฉพาะเจาะจงกับ Haskell เพราะความจริงที่ว่าการพับกระดาษด้านขวายุติลงหากฟังก์ชั่นการรวมไม่ได้ประเมินอาร์กิวเมนต์ที่สองนั้นเกิดจากกลยุทธ์การประเมินที่ขี้เกียจของ Haskell ฉันไม่ทราบ Clojure แต่ฉันเชื่อว่ามันใช้การประเมินผลที่เข้มงวดดังนั้นฉันคาดหวังว่ามันจะมีfoldฟังก์ชั่นในไลบรารีมาตรฐานที่มีการสนับสนุนเฉพาะสำหรับการเลิกจ้างก่อนกำหนด นี้มักจะถูกเรียกว่าfoldWhile, foldUntilหรือคล้ายกัน

ดูอย่างรวดเร็วที่เอกสารห้องสมุด Clojure แสดงให้เห็นว่ามันเป็นเรื่องที่แตกต่างกันเล็ก ๆ น้อย ๆ จากภาษาส่วนใหญ่ทำงานในการตั้งชื่อและที่foldไม่ได้เป็นสิ่งที่คุณกำลังมองหา (มันเป็นกลไกที่สูงขึ้นมุ่งเป้าไปที่การเปิดใช้การคำนวณแบบขนาน) แต่reduceเป็นโดยตรงมากขึ้น เท่ากัน การเลิกจ้างก่อนกำหนดเกิดขึ้นหากreducedมีการเรียกใช้ฟังก์ชันภายในฟังก์ชันการรวมของคุณ ฉันไม่แน่ใจ 100% ว่าฉันเข้าใจไวยากรณ์ แต่ฉันสงสัยว่าสิ่งที่คุณกำลังมองหาคืออะไร:

(reduce 
    (fn [v1 v2]
        (if (= v1 10) 
             (reduced v1)
             (+ v1 v2 (if (= v1 150) 100 0))))
    array)

หมายเหตุ: ทั้งการแปล Haskell และ Clojure นั้นค่อนข้างไม่ถูกต้องสำหรับรหัสเฉพาะนี้ แต่พวกเขานำเสนอสาระสำคัญทั่วไปของมัน - ดูการสนทนาในความคิดเห็นด้านล่างสำหรับปัญหาเฉพาะกับตัวอย่างเหล่านี้


11
ชื่อv1 v2มีความสับสน: v1เป็น "ค่าจากอาร์เรย์" แต่v2เป็นผลลัพธ์สะสม และการแปลของคุณเป็นสิ่งที่ผิดพลาดผมเชื่อว่าของ OP ออกจากวงเมื่อสะสม(จากซ้าย)มูลค่าฮิต 10 ไม่องค์ประกอบบางอย่างในอาร์เรย์ เดียวกันกับที่เพิ่มขึ้นด้วย 100 ถ้าจะใช้พับที่นี่ใช้ซ้ายพับกับออกก่อนการเปลี่ยนแปลงบางอย่างในที่นี่foldlWhile
Will Ness

2
มันเป็นเรื่องตลกที่คำตอบที่ผิดมากที่สุดได้รับ upvotes มากที่สุดใน SE .... ก็โอเคที่จะทำผิดพลาดคุณอยู่ในบริษัท ที่ดี :)เช่นกัน แต่กลไกการค้นพบความรู้ใน SO / SE นั้นแตกหักอย่างแน่นอน
Will Ness

1
รหัส Clojure เกือบถูกต้อง แต่เงื่อนไขการ(= v1 150)ใช้งานค่าก่อนหน้าv2(aka. e) จะถูกรวมเข้ากับมัน
NikoNyrh

1
Breaking this down line by line in case you're not familiar with Haskell's syntax- คุณคือฮีโร่ของฉัน Haskell เป็นปริศนาสำหรับฉัน
Captain Man

15
@ WillNess มัน upvoted เนื่องจากเป็นคำแปลและคำอธิบายที่เข้าใจได้ง่ายที่สุด ความจริงที่ว่ามันผิดคือความอับอาย แต่ค่อนข้างสำคัญเพราะข้อผิดพลาดเล็กน้อยไม่ได้ลบล้างความจริงที่ว่าคำตอบนั้นมีประโยชน์ แต่แน่นอนมันควรได้รับการแก้ไข
Konrad Rudolph

33

คุณสามารถแปลงเป็น recursion ได้อย่างง่ายดาย และมีการเรียกซ้ำแบบหางที่ปรับให้เหมาะสม

รหัสเทียม:

public int doSomeCalc(int[] array)
{
    return doSomeCalcInner(array, 0);
}

public int doSomeCalcInner(int[] array, int answer)
{
    if (array is empty) return answer;

    // not sure how to efficiently implement head/tails array split in clojure
    var head = array[0] // first element of array
    var tail = array[1..] // remainder of array

    answer += head;
    if (answer == 10) return answer;
    if (answer == 150) answer += 100;

    return doSomeCalcInner(tail, answer);
}

14
อ๋อ ฟังก์ชันที่เทียบเท่ากับลูปคือการวนรอบแบบวนซ้ำและการทำงานเทียบเท่ากับเงื่อนไขนั้นยังคงเป็นเงื่อนไข
Jörg W Mittag

4
@ GOTOJörgWMittagฉันอยากจะบอกว่าการเรียกซ้ำหางเป็นเทียบเท่าทำงานเพื่อ (ไม่เลวร้ายนัก แต่ก็ค่อนข้างน่าอึดอัดใจ) การเปรียบเทียบกับลูปอย่างที่จูลส์บอกว่าเป็นการพับที่เหมาะสม
leftaroundabout

6
@leftaroundabout ฉันไม่เห็นด้วยจริงๆ ฉันจะบอกว่าการเรียกซ้ำแบบหางมีข้อ จำกัด มากกว่าการข้ามไปเนื่องจากต้องข้ามไปที่ตัวเองและอยู่ในตำแหน่งหางเท่านั้น มันเป็นพื้นฐานเทียบเท่ากับการสร้างลูป GOTOผมว่าการเรียกซ้ำโดยทั่วไปจะเทียบเท่ากับ ไม่ว่าในกรณีใดเมื่อคุณคอมไพล์หางวนซ้ำมันส่วนใหญ่ก็จะวนไปเรื่อย ๆ เป็นwhile (true)วงเดียวกับส่วนของฟังก์ชั่นที่ซึ่งการคืนต้นเป็นเพียงbreakคำสั่ง การพับในขณะที่คุณถูกต้องเกี่ยวกับการวนซ้ำนั้นมีข้อ จำกัด มากกว่าการสร้างลูปทั่วไป มันเป็นเหมือนห่วงสำหรับแต่ละวง
J_mie6

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

1
... สำหรับโฟลด์: คุณถูกต้องโฟลด์แบบดั้งเดิม (catamorphism) เป็นวงที่เฉพาะเจาะจงมาก แต่แผนการเรียกซ้ำเหล่านี้สามารถเป็นแบบทั่วไป (ana- / apo- / hylomorphisms); เรียกรวมกันว่า IMO เป็นสิ่งทดแทนที่เหมาะสมสำหรับลูปที่จำเป็น
leftaroundabout

13

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

baseSums = scanl (+) 0

offsets = scanl (\offset sum -> if sum == 150 then offset + 100 else offset) 0

zipWithOffsets xs = zipWith (+) xs (offsets xs)

stopAt10 xs = if 10 `elem` xs then 10 else last xs

result = stopAt10 . zipWithOffsets . baseSums

result [1..]         -- 10
result [11..1000000] -- 500000499945

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


ตรรกะกระจัดกระจายไปหมดที่นี่ รหัสนี้จะไม่ง่ายต่อการบำรุงรักษา stopAt10คือไม่ได้เป็นผู้บริโภคที่ดี คำตอบของคุณคือดีกว่าคนที่คุณกล่าวอ้างในการที่จะได้อย่างถูกต้องแยกผู้ผลิตพื้นฐานscanl (+) 0ของค่านิยม การบริโภคของพวกเขาควรรวมตรรกะการควบคุมโดยตรงแม้ว่าจะมีการใช้งานที่ดีขึ้นด้วยสองspans และ a lastอย่างชัดเจน ที่จะติดตามโครงสร้างรหัสและตรรกะดั้งเดิมอย่างใกล้ชิดเช่นกันและง่ายต่อการบำรุงรักษา
Will Ness

6

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

นี่เป็นการแปลโดยตรงไปยัง Clojure:

(defn doSomeCalc 
  ([lst] (doSomeCalc lst 0))
  ([lst sum]
    (if (empty? lst) sum
        (if (= sum 10) sum
            (let [sum (+ sum (first lst))]
                 [sum (if (= sum 150) (+ sum 100) sum)]
               (recur (rest lst) sum))))))) 

แก้ไข: จูลส์ชี้ให้เห็นว่าreduceใน Clojure จะสนับสนุนการออกก่อน การใช้สิ่งนี้จะสวยงามยิ่งกว่า:

(defn doSomeCalc [lst]  
  (reduce (fn [sum val]
    (if (= sum 10) (reduced sum)
        (let [sum (+ sum val)]
             [sum (if (= sum 150) (+ sum 100) sum)]
           sum))
   lst)))

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


ดูการแก้ไขที่ฉันเพิ่งเพิ่มไปยังคำตอบของฉัน: reduceการทำงานของ Clojure รองรับการออกก่อนกำหนด
Jules

@ จูล: เย็น - นั่นอาจเป็นวิธีแก้ปัญหาสำนวนมากขึ้น
JacquesB

ไม่ถูกต้อง - หรือtakeWhileไม่ใช่ 'การดำเนินงานทั่วไป'?
Jonathan Cast

@jcast - ในขณะที่takeWhileเป็นการดำเนินการทั่วไปจะไม่เป็นประโยชน์โดยเฉพาะอย่างยิ่งในกรณีนี้เพราะคุณต้องการผลลัพธ์ของการเปลี่ยนแปลงของคุณก่อนที่คุณจะตัดสินใจว่าจะหยุด ในภาษาที่ขี้เกียจสิ่งนี้ไม่สำคัญ: คุณสามารถใช้scanและtakeWhileกับผลลัพธ์ของการสแกน (ดูคำตอบของ Karl Bielefeldt ซึ่งในขณะที่มันไม่ได้ใช้takeWhileก็สามารถเขียนใหม่ได้อย่างง่ายดาย) แต่สำหรับภาษาที่เข้มงวดเช่นนี้ หมายถึงการประมวลผลรายการทั้งหมดแล้วทิ้งผลลัพธ์ในภายหลัง อย่างไรก็ตามฟังก์ชั่นตัวสร้างสามารถแก้ปัญหานี้ได้และฉันเชื่อว่า Clojure รองรับพวกมัน
Jules

@Jules take-whilein Clojure สร้างลำดับที่ขี้เกียจ (ตามเอกสาร) อีกวิธีหนึ่งในการแก้ไขปัญหานี้ก็คือกับทรานสดิวเซอร์ (อาจเป็นวิธีที่ดีที่สุด)
Will Ness

4

ตามที่ระบุไว้โดยคำตอบอื่น ๆ Clojure ได้reducedหยุดการลดต้น:

(defn some-calc [coll]
  (reduce (fn [answer e]
            (let [answer (+ answer e)]
               (case answer
                 10  (reduced answer)
                 150 (+ answer 100)
                 answer)))
          0 coll))

นี่เป็นทางออกที่ดีที่สุดสำหรับสถานการณ์ของคุณโดยเฉพาะ คุณยังสามารถได้รับมากระยะจากการรวมreducedกับtransduceซึ่งจะช่วยให้คุณใช้ก้อนจากmap, filterฯลฯ แต่มันอยู่ไกลจากคำตอบที่สมบูรณ์คำถามทั่วไปของคุณ

Escape continuations เป็นเวอร์ชันทั่วไปของคำสั่ง break และ return พวกเขาจะดำเนินการโดยตรงในบาง Schemes ( call-with-escape-continuation), Common LISP ( block+ return, catch+ throw) และแม้กระทั่ง C ( setjmp+ longjmp) ตัวคั่นที่มีตัวคั่นหรือตัวคั่นที่ไม่ จำกัด ที่พบใน Scheme มาตรฐานหรือเป็น Monads ต่อเนื่องใน Haskell และ Scala ยังสามารถใช้เป็นตัวต่อเนื่องในการหลบหนี

ตัวอย่างเช่นใน Racket คุณสามารถใช้let/ecสิ่งนี้:

(define (some-calc ls)
  (let/ec break ; let break be an escape continuation
    (foldl (lambda (answer e)
             (let ([answer (+ answer e)])
               (case answer
                 [(10)  (break answer)] ; return answer immediately
                 [(150) (+ answer 100)]
                 [else  answer])))
           0 ls)))

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

นอกจากนี้คุณยังสามารถเลื่อนลงจากฟังก์ชั่นการสั่งซื้อที่สูงขึ้นเพื่อรับสาย

เมื่อใช้ลูปคุณป้อนการวนซ้ำครั้งถัดไปโดยอัตโนมัติเมื่อถึงจุดสิ้นสุดของลูป คุณสามารถเข้าสู่การวนซ้ำครั้งถัดไปก่อนด้วยcontinueหรือออกจากลูปด้วยbreak(หรือreturn) เมื่อใช้การเรียกแบบหาง (หรือการloopสร้างของ Clojure ซึ่งการจำลองแบบหางซ้ำ) คุณต้องทำการเรียกที่ชัดเจนเพื่อเข้าสู่การวนซ้ำครั้งถัดไป หากต้องการหยุดการวนซ้ำคุณไม่ต้องโทรซ้ำ แต่ให้ค่าโดยตรง:

(defn some-calc [coll]
  (loop [answer 0, [e es :as coll] coll]
    (if (empty? coll)
      answer
      (let [answer (+ answer e)]
        (case answer
          10 answer
          150 (recur (+ answer 100) es)
          (recur answer es))))))

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

@Jules ฉันคิดว่าการย้อนกลับ Left ไม่ได้ป้องกันการพับจากการเยี่ยมชมรายการทั้งหมด (หรือลำดับอื่น ๆ ) ไม่คุ้นเคยอย่างใกล้ชิดกับ internals ไลบรารีมาตรฐาน Haskell
nilern

2

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

นี่คือการใช้งานของลูปทั่วไป:

loop : v -> (v -> v) -> (v -> Bool) -> v
loop init iter cond_to_cont = 
    if cond_to_cont init 
        then loop (iter init) iter cond
        else init

ใช้เวลา (ค่าเริ่มต้นของตัวแปรลูปฟังก์ชันที่แสดงการวนซ้ำเดียว [บนตัวแปรลูป]) (เงื่อนไขเพื่อดำเนินการต่อลูป)

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

module Array (foldlc) where

foldlc : v -> (v -> e -> v) -> (v -> Bool) -> Array e -> v
foldlc init iter cond_to_cont arr = 
    loop 
        (init, 0)
        (λ (val, next_pos) -> (iter val (at next_pos arr), next_pos + 1))
        (λ (val, next_pos) -> and (cond_to_cont val) (next_pos < size arr))

ในนั้น :

ฉันใช้คู่ ((val, next_pos)) ซึ่งมีตัวแปรลูปที่มองเห็นภายนอกและตำแหน่งในอาเรย์ซึ่งฟังก์ชั่นนี้ซ่อนอยู่

ฟังก์ชั่นการวนซ้ำนั้นซับซ้อนกว่าลูปทั่วไปเล็กน้อยรุ่นนี้ทำให้สามารถใช้องค์ประกอบปัจจุบันของอาร์เรย์ได้ [มันอยู่ในรูปแบบcurried ]

ฟังก์ชั่นดังกล่าวมักจะมีชื่อว่า "fold"

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

ฉันใส่ "c" ในชื่อเพื่อระบุว่า fold รุ่นนี้มีเงื่อนไขที่ควบคุมว่าและเมื่อจะหยุดลูปก่อนหรือไม่

แน่นอนว่าฟังก์ชั่นยูทิลิตี้ดังกล่าวมีแนวโน้มว่าจะพร้อมใช้งานในไลบรารีฐานที่มาพร้อมกับภาษาการเขียนโปรแกรมการทำงานที่ใช้ ฉันเขียนพวกเขาที่นี่เพื่อสาธิต

ตอนนี้เรามีเครื่องมือทั้งหมดที่เป็นภาษาในกรณีจำเป็นเราสามารถหันไปใช้ฟังก์ชั่นเฉพาะของตัวอย่างของคุณ

ตัวแปรในลูปของคุณเป็นคู่ ('answer', บูลีนที่เข้ารหัสว่าจะดำเนินการต่อ)

iter : (Int, Bool) -> Int -> (Int, Bool)
iter (answer, cont) collection_element = 
  let new_answer = answer + collection_element
  in case new_answer of
    10 -> (new_answer, false)
    150 -> (new_answer + 100, true)
    _ -> (new_answer, true)

โปรดทราบว่าฉันใช้ "ตัวแปร" ใหม่ new_answer ' นี่เป็นเพราะในการเขียนโปรแกรมการทำงานฉันไม่สามารถเปลี่ยนค่าของ "ตัวแปร" เริ่มต้นแล้ว ฉันไม่กังวลเกี่ยวกับประสิทธิภาพคอมไพเลอร์อาจนำหน่วยความจำของ 'คำตอบ' สำหรับ 'new_answer' กลับมาใช้ใหม่ผ่านการวิเคราะห์ตลอดชีวิตหากคิดว่ามีประสิทธิภาพมากขึ้น

การรวมสิ่งนี้เข้ากับฟังก์ชั่นการวนซ้ำของเราที่พัฒนาขึ้นก่อนหน้านี้:

doSomeCalc :: Array Int -> Int
doSomeCalc arr = fst (Array.foldlc (0, true) iter snd arr)

"Array" ที่นี่คือชื่อโมดูลที่ส่งออกฟังก์ชัน foldlc

"fist", "second" หมายถึงฟังก์ชั่นที่ส่งกลับองค์ประกอบที่หนึ่งและสองของพารามิเตอร์ pair

fst : (x, y) -> x
snd : (x, y) -> y

ในกรณีนี้สไตล์ "ไม่มีจุด" จะเพิ่มความสามารถในการอ่านของการใช้ doSomeCalc:

doSomeCalc = Array.foldlc (0, true) iter snd >>> fst

(>>>) คือฟังก์ชั่นองค์ประกอบ: (>>>) : (a -> b) -> (b -> c) -> (a -> c)

มันเป็นเช่นเดียวกับข้างต้นเพียงแค่พารามิเตอร์ "arr" จะถูกปล่อยออกจากทั้งสองด้านของสมการที่กำหนด

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


0

ขั้นแรกให้เขียนลูปใหม่เล็กน้อยเช่นให้วนซ้ำของลูปก่อนออกก่อนหรือเปลี่ยนanswerเป็นครั้งเดียว:

    public int doSomeCalc(int[] array)
    {
        int answer = 0;
        if(array!=null)
        {
            for(int e: array)
            {
                if(answer + e == 10) return answer + e;
                else if(answer + e == 150) answer = answer + e + 100;
                else answer = answer + e;
            }
        }
        return answer;
    }

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

doSomeCalc :: [Int] -> Int
doSomeCalc = recurse 0
  where recurse :: Int -> [Int] -> Int
        recurse answer [] = answer
        recurse answer (e:array)
          | answer + e == 10 = answer + e
          | answer + e == 150 = recurse (answer + e + 100) array
          | otherwise = recurse (answer + e) array

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

import Control.Monad (foldM)

doSomeCalc :: [Int] -> Int
doSomeCalc = either id id . foldM go 0
  where go :: Int -> Int -> Either Int Int
        go answer e
          | answer + e == 10 = Left (answer + e)
          | answer + e == 150 = Right (answer + e + 100)
          | otherwise = Right (answer + e)

ในบริบทนี้Leftออกก่อนเวลาด้วยค่าของมันและRightดำเนินการเรียกซ้ำตามค่าของมัน


สิ่งนี้สามารถทำให้ง่ายขึ้นอีกเล็กน้อยเช่นนี้

import Control.Monad (foldM)

doSomeCalc :: [Int] -> Int
doSomeCalc = either id id . foldM go 0
  where go :: Int -> Int -> Either Int Int
        go answer e
          | answer' == 10 = Left 10
          | answer' == 150 = Right 250
          | otherwise = Right answer'
          where answer' = answer + e

นี่เป็นโค้ด Haskell ขั้นสุดท้ายที่ดีกว่า แต่ตอนนี้มันค่อนข้างชัดเจนน้อยลงว่าจะจับคู่กลับไปที่ Java ดั้งเดิมอย่างไร

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