Các tương đương chức năng của các câu lệnh phá vỡ mệnh lệnh và kiểm tra vòng lặp khác là gì?


36

Hãy nói rằng, tôi có logic dưới đây. Làm thế nào để viết điều đó trong Lập trình hàm?

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

Các ví dụ trong hầu hết các blog, bài viết ... Tôi thấy chỉ giải thích trường hợp đơn giản của một hàm toán học chuyển tiếp thẳng là 'Tổng'. Nhưng, tôi có một logic tương tự như ở trên được viết bằng Java và muốn chuyển nó sang mã chức năng trong Clojure. Nếu chúng ta không thể thực hiện những điều trên trong FP, thì loại khuyến mãi cho FP không nêu rõ điều này.

Tôi biết rằng các mã trên là hoàn toàn bắt buộc. Nó không được viết với ý nghĩ di chuyển nó sang FP trong tương lai.


1
Lưu ý rằng sự kết hợp breakreturn answercó thể được thay thế bằng một returnvòng lặp. Trong FP, bạn có thể thực hiện việc hoàn trả sớm này bằng cách sử dụng các phần tiếp theo, xem ví dụ en.wikipedia.org/wiki/Contininating
Giorgio

1
@Giorgio tiếp tục sẽ là một quá mức cần thiết ở đây. Dù sao đó cũng là một vòng lặp, để gọi lần lặp tiếp theo của nó, bạn thực hiện một cuộc gọi đuôi, vì vậy để phá vỡ bạn chỉ cần không gọi nó nữa và chỉ cần trả lời câu trả lời. Đối với các vòng lặp lồng nhau hoặc luồng điều khiển phức tạp khác, đó là nơi bạn có thể sử dụng các phần tiếp theo thay vì tập trung để cơ cấu lại mã của mình để sử dụng kỹ thuật đơn giản ở trên (điều này luôn luôn có thể, nhưng có thể dẫn đến cấu trúc mã quá phức tạp sẽ ít nhiều giải thích sự tiếp tục và cho nhiều hơn một điểm thoát bạn chắc chắn cần chúng).
Will Ness

8
Trong trường hợp này : takeWhile.
Jonathan Cast

1
@WillNess: Tôi chỉ muốn đề cập đến nó bởi vì nó có thể được sử dụng để để lại một tính toán phức tạp tại bất kỳ thời điểm nào. Nó có lẽ không phải là giải pháp tốt nhất cho ví dụ cụ thể của OP.
Giorgio

@Giorgio bạn nói đúng, nói chung là toàn diện nhất. thực sự câu hỏi này rất rộng, IYKWIM (tức là sẽ bị đóng trên SO trong tích tắc).
Will Ness

Câu trả lời:


45

Tương đương gần nhất với việc lặp qua một mảng trong hầu hết các ngôn ngữ chức năng là một foldhàm, tức là một hàm gọi hàm do người dùng chỉ định cho mỗi giá trị của mảng, chuyển một giá trị tích lũy dọc theo chuỗi. Trong nhiều ngôn ngữ chức năng, foldđược tăng cường bởi một loạt các chức năng bổ sung cung cấp các tính năng bổ sung, bao gồm tùy chọn dừng sớm khi một số điều kiện phát sinh. Trong các ngôn ngữ lười biếng (ví dụ: Haskell), việc dừng sớm có thể đạt được chỉ bằng cách không đánh giá thêm bất kỳ danh sách nào nữa, điều này sẽ khiến các giá trị bổ sung không bao giờ được tạo ra. Do đó, dịch ví dụ của bạn sang Haskell, tôi sẽ viết nó thành:

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

Chia dòng này xuống từng dòng trong trường hợp bạn không quen với cú pháp của Haskell, cách này hoạt động như sau:

doSomeCalc :: [Int] -> Int

Xác định loại hàm, chấp nhận danh sách các số nguyên và trả về một số nguyên.

doSomeCalc values = foldr1 combine values

Phần chính của hàm: đối số đã cho values, trả về foldr1được gọi bằng đối số combine(mà chúng ta sẽ xác định bên dưới) và values. foldr1là một biến thể của nguyên thủy gấp bắt đầu với bộ tích lũy được đặt thành giá trị đầu tiên của danh sách (do đó 1trong tên hàm), sau đó kết hợp nó bằng cách sử dụng hàm do người dùng chỉ định từ trái sang phải (thường được gọi là nếp gấp bên phải , do đó rtrong tên hàm). Vì vậy, foldr1 f [1,2,3]tương đương với f 1 (f 2 3)(hoặc f(1,f(2,3))theo cú pháp giống C thông thường hơn).

  where combine v1 v2 | v1 == 10  = v1

Xác định combinehàm cục bộ: nó nhận được hai đối số v1v2. Khi v1là 10, nó chỉ trở về v1. Trong trường hợp này, v2 không bao giờ được đánh giá , vì vậy vòng lặp dừng ở đây.

                      | v1 == 150 = v1 + 100 + v2

Ngoài ra, khi v1 là 150, thêm 100 vào đó và thêm v2.

                      | otherwise = v1 + v2

Và, nếu cả hai điều kiện này đều không đúng, chỉ cần thêm v1 vào v2.

Bây giờ, giải pháp này có phần cụ thể đối với Haskell, vì thực tế là một nếp gấp bên phải chấm dứt nếu chức năng kết hợp không đánh giá đối số thứ hai của nó là do chiến lược đánh giá lười biếng của Haskell. Tôi không biết Clojure, nhưng tôi tin rằng nó sử dụng đánh giá nghiêm ngặt, vì vậy tôi hy vọng nó có foldchức năng trong thư viện tiêu chuẩn bao gồm hỗ trợ cụ thể cho việc chấm dứt sớm. Điều này thường được gọi foldWhile, foldUntilhoặc tương tự.

Nhìn nhanh vào tài liệu thư viện Clojure cho thấy nó hơi khác so với hầu hết các ngôn ngữ chức năng trong cách đặt tên và đó foldkhông phải là thứ bạn đang tìm kiếm (đó là một cơ chế tiên tiến hơn nhằm cho phép tính toán song song) nhưng reducetrực tiếp hơn tương đương. Chấm dứt sớm xảy ra nếu reducedchức năng được gọi trong chức năng kết hợp của bạn. Tôi không chắc chắn 100% tôi hiểu cú pháp, nhưng tôi nghi ngờ những gì bạn đang tìm kiếm là một cái gì đó như thế này:

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

NB: cả hai bản dịch, Haskell và Clojure, không hoàn toàn đúng với mã cụ thể này; nhưng họ truyền đạt ý chính chung của nó - xem thảo luận trong các bình luận bên dưới để biết các vấn đề cụ thể với các ví dụ này.


11
các tên v1 v2khó hiểu: v1là "giá trị từ mảng", nhưng v2là kết quả tích lũy. và bản dịch của bạn là sai, tôi tin rằng, vòng lặp của OP sẽ thoát khi giá trị tích lũy (từ trái sang) chạm 10, chứ không phải một số phần tử trong mảng. Tương tự với mức tăng thêm 100. Nếu để sử dụng các nếp gấp ở đây, hãy sử dụng nếp gấp bên trái với lối ra sớm, một số biến thể foldlWhile ở đây .
Will Ness

2
hài hước như thế nào câu trả lời sai nhất được upvotes nhất trên SE .... nó OK để phạm sai lầm, bạn đang ở công ty tốt :) , quá. Nhưng cơ chế khám phá kiến ​​thức về SO / SE chắc chắn bị phá vỡ.
Will Ness

1
Mã Clojure gần như đúng, nhưng điều kiện (= v1 150)sử dụng giá trị trước đó v2(hay còn gọi là e) được tóm tắt với nó.
NikoNyrh

1
Breaking this down line by line in case you're not familiar with Haskell's syntax-- Bạn là người hùng của tôi. Haskell là một bí ẩn đối với tôi.
Thuyền trưởng Man

15
@WillNess Nó được nâng cấp bởi vì đó là bản dịch và giải thích dễ hiểu nhất. Thực tế là nó sai là một sự xấu hổ nhưng tương đối không quan trọng ở đây vì những lỗi nhỏ không phủ nhận thực tế rằng câu trả lời là hữu ích. Nhưng tất nhiên nó nên được sửa chữa.
Konrad Rudolph

33

Bạn có thể dễ dàng chuyển đổi nó thành đệ quy. Và nó có cuộc gọi đệ quy được tối ưu hóa đuôi đẹp.

Mã giả:

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
Vâng. Hàm tương đương với một vòng lặp là đệ quy đuôi và hàm tương đương với một điều kiện vẫn là một điều kiện.
Jörg W Mittag

4
@ JörgWMittag Tôi muốn nói rằng đệ quy đuôi là chức năng tương đương GOTO. (Không hoàn toàn xấu, nhưng vẫn khá khó xử.) Tương đương với một vòng lặp, như Jules nói, là một nếp gấp phù hợp.
rẽ trái

6
@leftaroundabout Tôi thực sự không đồng ý. Tôi muốn nói đệ quy đuôi bị hạn chế hơn một goto, do cần phải tự nhảy và chỉ ở vị trí đuôi. Nó về cơ bản tương đương với một cấu trúc lặp. Tôi muốn nói đệ quy nói chung là tương đương với GOTO. Trong mọi trường hợp, khi bạn biên dịch đệ quy đuôi, phần lớn chỉ cần luồn xuống một while (true)vòng lặp với thân hàm trong đó việc trả về sớm chỉ là một breakcâu lệnh. Một nếp gấp, trong khi bạn đúng về việc nó là một vòng lặp, thực sự bị ràng buộc nhiều hơn so với cấu trúc vòng lặp chung; nó giống như một vòng lặp for - mỗi vòng lặp
J_mie6

1
@ J_mie6 lý do tôi xem xét đệ quy đuôi nhiều hơn GOTOlà vì bạn cần thực hiện ghi sổ một cách khéo léo về những đối số trong trạng thái nào được chuyển đến lệnh gọi đệ quy, để đảm bảo nó thực sự hoạt động như dự định. Điều đó là không cần thiết ở cùng một mức độ trong các vòng lặp mệnh lệnh được viết rõ ràng (trong đó khá rõ các biến trạng thái là gì và chúng thay đổi như thế nào trong mỗi lần lặp), cũng như trong đệ quy ngây thơ (thường không được thực hiện nhiều với các đối số, và thay vào đó kết quả được lắp ráp một cách khá trực quan). ...
leftaroundabout

1
... Về nếp gấp: bạn nói đúng, nếp gấp truyền thống (catamorphism) là một loại vòng lặp rất cụ thể, nhưng các sơ đồ đệ quy này có thể được khái quát hóa (ana- / apo- / hylomorphism); gọi chung là IMO sự thay thế thích hợp cho các vòng lặp bắt buộc.
rẽ trái

13

Tôi thực sự thích câu trả lời của Jules , nhưng tôi muốn chỉ ra thêm một điều mà mọi người thường bỏ lỡ về lập trình chức năng lười biếng, đó là mọi thứ không nhất thiết phải nằm trong "vòng lặp". Ví dụ:

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

Bạn có thể thấy rằng mỗi phần logic của bạn có thể được tính trong một hàm riêng biệt sau đó được kết hợp với nhau. Điều này cho phép các chức năng nhỏ hơn thường dễ khắc phục sự cố hơn. Đối với ví dụ đồ chơi của bạn, có lẽ điều này làm tăng thêm độ phức tạp so với loại bỏ, nhưng trong mã thế giới thực, các hàm phân tách thường đơn giản hơn nhiều so với toàn bộ.


logic nằm rải rác khắp đây. mã này sẽ không dễ bảo trì. stopAt10không một người tiêu dùng tốt. Câu trả lời của bạn tốt hơn so với một bạn trích dẫn ở chỗ nó phân lập một cách chính xác là nhà sản xuất cơ bản scanl (+) 0của giá trị. việc tiêu thụ của họ nên kết hợp trực tiếp logic điều khiển, được triển khai tốt hơn chỉ với hai spangiây và một last, rõ ràng. điều đó cũng theo sát cấu trúc mã và logic ban đầu, và dễ bảo trì.
Will Ness

6

Hầu hết các ví dụ xử lý danh sách, bạn sẽ thấy các chức năng sử dụng như map, filter, sumvv mà hoạt động trên danh sách như một toàn thể. Nhưng trong trường hợp của bạn, bạn có một lối ra sớm có điều kiện - một mô hình khá phổ biến không được hỗ trợ bởi các hoạt động danh sách thông thường. Vì vậy, bạn cần phải giảm xuống một mức độ trừu tượng và sử dụng đệ quy - cũng gần với ví dụ mệnh lệnh trông như thế nào.

Đây là một bản dịch khá trực tiếp (có thể không phải là thành ngữ) sang 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))))))) 

Chỉnh sửa: Jules điểm ra rằng reducetrong Clojure làm hỗ trợ thoát sớm. Sử dụng này là thanh lịch hơn:

(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)))

Trong mọi trường hợp, bạn có thể làm bất cứ điều gì trong các ngôn ngữ chức năng như bạn có thể bằng các ngôn ngữ bắt buộc, nhưng bạn thường phải thay đổi suy nghĩ của mình một chút để tìm ra một giải pháp tao nhã. Trong mã hóa bắt buộc, bạn nghĩ đến việc xử lý danh sách từng bước, trong khi trong các ngôn ngữ chức năng, bạn tìm kiếm một thao tác để áp dụng cho tất cả các thành phần trong danh sách.


xem bản chỉnh sửa tôi vừa thêm vào câu trả lời của mình: reduceHoạt động của Clojure hỗ trợ thoát sớm.
Jules

@Jules: Tuyệt vời - đó có lẽ là một giải pháp thành ngữ hơn.
JacquesB

Không chính xác - hoặc takeWhilekhông phải là 'hoạt động chung'?
Jonathan Cast

@jcast - trong khi takeWhilelà một hoạt động phổ biến, nó không đặc biệt hữu ích trong trường hợp này, bởi vì bạn cần kết quả chuyển đổi của mình trước khi bạn có thể quyết định có dừng hay không. Trong một ngôn ngữ lười biếng, điều này không thành vấn đề: bạn có thể sử dụng scantakeWhiletrên kết quả quét (xem câu trả lời của Karl Bielefeldt, trong khi nó không sử dụng takeWhilecó thể dễ dàng viết lại để làm như vậy), nhưng đối với một ngôn ngữ nghiêm ngặt như clojure thì điều này sẽ có nghĩa là xử lý toàn bộ danh sách và sau đó loại bỏ kết quả sau đó. Các chức năng của trình tạo có thể giải quyết điều này, tuy nhiên, và tôi tin rằng clojure hỗ trợ chúng.
Jules

@Jules take-whiletrong Clojure tạo ra một chuỗi lười biếng (theo các tài liệu). Một cách khác để giải quyết vấn đề này là với đầu dò (có thể là cách tốt nhất).
Will Ness

4

Như được chỉ ra bởi các câu trả lời khác, Clojure đã reduceddừng việc giảm sớm:

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

Đây là giải pháp tốt nhất cho tình huống cụ thể của bạn. Bạn cũng có thể nhận được rất nhiều số dặm từ việc kết hợp reducedvới transduce, cho phép bạn sử dụng các bộ chuyển đổi từ map, filterv.v. Tuy nhiên, nó còn lâu mới có câu trả lời hoàn chỉnh cho câu hỏi chung của bạn.

Escape continuations là một phiên bản tổng quát của các câu lệnh break and return. Chúng được thực hiện trực tiếp trong một số Scheme ( call-with-escape-continuation), Common Lisp ( block+ return, catch+ throw) và thậm chí C ( setjmp+ longjmp). Các phần tiếp theo được phân tách hoặc không giới hạn chung như được tìm thấy trong Lược đồ tiêu chuẩn hoặc như các đơn vị tiếp tục trong Haskell và Scala cũng có thể được sử dụng làm phần tiếp theo thoát.

Ví dụ: trong Vợt bạn có thể sử dụng let/ecnhư thế này:

(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)))

Nhiều ngôn ngữ khác cũng có các cấu trúc giống như tiếp tục thoát dưới dạng xử lý ngoại lệ. Trong Haskell, bạn cũng có thể sử dụng một trong các đơn vị lỗi khác nhau với foldM. Bởi vì chúng chủ yếu là các cấu trúc xử lý lỗi sử dụng các ngoại lệ hoặc các đơn vị lỗi để trả về sớm thường không được chấp nhận về mặt văn hóa và có thể khá chậm.

Bạn cũng có thể thả xuống từ các hàm bậc cao hơn đến các cuộc gọi đuôi.

Khi sử dụng các vòng lặp, bạn sẽ tự động nhập lần lặp tiếp theo khi bạn đến cuối thân vòng lặp. Bạn có thể nhập lặp lại tiếp theo sớm bằng continuehoặc thoát khỏi vòng lặp với break(hoặc return). Khi sử dụng các cuộc gọi đuôi (hoặc loopcấu trúc của Clojure bắt chước đệ quy đuôi), bạn phải luôn thực hiện một cuộc gọi rõ ràng để vào lần lặp tiếp theo. Để dừng lặp, bạn chỉ không thực hiện cuộc gọi đệ quy mà cung cấp giá trị trực tiếp:

(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
Đang sử dụng các đơn vị lỗi trong Haskell, tôi không tin có bất kỳ hình phạt hiệu suất thực sự nào ở đây. Họ có xu hướng nghĩ về việc xử lý ngoại lệ, nhưng họ không làm việc theo cùng một cách và không có bất kỳ yêu cầu đi bộ nào, vì vậy thực sự không nên là vấn đề nếu sử dụng theo cách này. Ngoài ra, ngay cả khi có một lý do văn hóa không sử dụng một cái gì đó như thế MonadError, thì tương đương về cơ bản Eitherkhông có sự thiên vị như vậy đối với việc xử lý lỗi, do đó có thể dễ dàng được sử dụng thay thế.
Jules

@Jules Tôi nghĩ rằng việc quay lại Left không ngăn cản việc truy cập toàn bộ danh sách (hoặc chuỗi khác). Không quen thuộc với nội bộ thư viện tiêu chuẩn Haskell.
nilern

2

Phần phức tạp là vòng lặp. Hãy để chúng tôi bắt đầu với điều đó. Một vòng lặp thường được chuyển đổi thành kiểu chức năng bằng cách thể hiện phép lặp với một hàm duy nhất. Lặp lại là một biến đổi của biến vòng lặp.

Đây là một thực hiện chức năng của một vòng lặp chung:

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

Nó nhận (một giá trị ban đầu của biến vòng lặp, hàm biểu thị một lần lặp duy nhất [trên biến vòng lặp]) (một điều kiện để tiếp tục vòng lặp).

Ví dụ của bạn sử dụng một vòng lặp trên một mảng, cũng phá vỡ. Khả năng này trong ngôn ngữ mệnh lệnh của bạn được đưa vào chính ngôn ngữ đó. Trong lập trình chức năng, khả năng như vậy thường được thực hiện ở cấp thư viện. Đây là một triển khai có thể

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))

Trong đó :

Tôi sử dụng cặp ((val, next_pose)) chứa biến vòng lặp hiển thị bên ngoài và vị trí trong mảng, hàm này ẩn.

Hàm lặp phức tạp hơn một chút so với trong vòng lặp chung, phiên bản này cho phép sử dụng phần tử hiện tại của mảng. [Nó ở dạng quăn .]

Các chức năng như vậy thường được đặt tên là "gấp".

Tôi đặt một chữ "l" trong tên để chỉ ra rằng việc tích lũy các phần tử của mảng được thực hiện theo cách liên kết trái; để bắt chước thói quen của các ngôn ngữ lập trình bắt buộc để lặp lại một mảng từ chỉ số thấp đến cao.

Tôi đặt một chữ "c" trong tên để chỉ ra rằng phiên bản gấp này có một điều kiện kiểm soát nếu và khi vòng lặp được dừng sớm.

Tất nhiên các chức năng tiện ích như vậy có khả năng sẽ có sẵn trong thư viện cơ sở được cung cấp với ngôn ngữ lập trình chức năng được sử dụng. Tôi đã viết chúng ở đây để trình diễn.

Bây giờ chúng ta có tất cả các công cụ bằng ngôn ngữ trong trường hợp bắt buộc, chúng ta có thể chuyển sang thực hiện chức năng cụ thể của ví dụ của bạn.

Biến trong vòng lặp của bạn là một cặp ('answer', boolean mã hóa xem có tiếp tục không).

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)

Lưu ý rằng tôi đã sử dụng một "biến" mới 'new_answer'. Điều này là do trong lập trình chức năng, tôi không thể thay đổi giá trị của một "biến" đã được khởi tạo. Tôi không lo lắng về hiệu suất, trình biên dịch có thể sử dụng lại bộ nhớ của 'answer' cho 'new_answer' thông qua phân tích trọn đời, nếu nó cho rằng nó hiệu quả hơn.

Kết hợp điều này vào chức năng lặp của chúng tôi đã phát triển trước đó:

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

"Mảng" ở đây là tên mô-đun xuất hàm Foldlc.

"fist", "second" là viết tắt của các hàm trả về thành phần thứ nhất, thứ hai của tham số cặp của nó

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

Trong trường hợp này, kiểu "không có điểm" làm tăng khả năng đọc của việc thực hiện doSomeCalc:

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

(>>>) là thành phần chức năng: (>>>) : (a -> b) -> (b -> c) -> (a -> c)

Nó giống như trên, chỉ là tham số "mảng" được bỏ qua từ cả hai phía của phương trình xác định.

Một điều cuối cùng: kiểm tra trường hợp (mảng == null). Trong các ngôn ngữ lập trình được thiết kế tốt hơn, nhưng ngay cả trong các ngôn ngữ được thiết kế tồi với một số môn học cơ bản, người ta sử dụng một loại tùy chọn để thể hiện sự không tồn tại. Điều này không liên quan nhiều đến lập trình chức năng, mà câu hỏi cuối cùng là về, do đó tôi không giải quyết nó.


0

Đầu tiên, viết lại vòng lặp một chút, sao cho mỗi lần lặp của vòng lặp thoát ra sớm hoặc biến đổi answerchính xác một lần:

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

Cần phải rõ ràng rằng hành vi của phiên bản này hoàn toàn giống như trước đây, nhưng bây giờ, việc chuyển đổi sang phong cách đệ quy trở nên đơn giản hơn nhiều. Đây là bản dịch trực tiếp của 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

Bây giờ nó hoàn toàn có chức năng, nhưng chúng tôi có thể cải thiện nó từ cả quan điểm hiệu quả và dễ đọc bằng cách sử dụng một nếp gấp thay vì đệ quy rõ ràng:

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)

Trong bối cảnh này, xuất cảnh Leftsớm với giá trị của nó và Righttiếp tục đệ quy với giá trị của nó.


Điều này bây giờ có thể được đơn giản hóa hơn một chút, như thế này:

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

Điều này tốt hơn như mã Haskell cuối cùng, nhưng giờ thì rõ ràng hơn một chút về cách nó ánh xạ trở lại Java ban đầu.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.