Clojure: giảm so với áp dụng


126

Tôi hiểu sự khác biệt về khái niệm giữa reduceapply:

(reduce + (list 1 2 3 4 5))
; translates to: (+ (+ (+ (+ 1 2) 3) 4) 5)

(apply + (list 1 2 3 4 5))
; translates to: (+ 1 2 3 4 5)

Tuy nhiên, cái nào là clojure thành ngữ hơn? Nó làm cho nhiều sự khác biệt theo cách này hay cách khác? Từ thử nghiệm hiệu suất (giới hạn) của tôi, có vẻ như reducenhanh hơn một chút.

Câu trả lời:


125

reduceapplytất nhiên chỉ tương đương (về kết quả cuối cùng được trả về) cho các hàm kết hợp cần xem tất cả các đối số của chúng trong trường hợp biến thiên. Khi chúng tương đương với kết quả, tôi muốn nói rằng applynó luôn luôn hoàn toàn thành ngữ, trong khi reducetương đương - và có thể loại bỏ một phần của một cái chớp mắt - trong rất nhiều trường hợp phổ biến. Điều gì sau đây là lý do của tôi để tin vào điều này.

+được thực hiện theo các điều khoản reducecho trường hợp biến thiên (hơn 2 đối số). Thật vậy, đây có vẻ như là một cách "mặc định" vô cùng hợp lý để sử dụng cho bất kỳ chức năng kết hợp, biến đổi nào: reducecó khả năng thực hiện một số tối ưu hóa để tăng tốc mọi thứ - có lẽ thông qua một cái gì đó như internal-reduce, một tính mới 1,2 gần đây đã bị vô hiệu hóa trong chủ, nhưng hy vọng sẽ được giới thiệu lại trong tương lai - điều sẽ thật ngớ ngẩn khi sao chép trong mọi chức năng có thể có lợi từ chúng trong trường hợp vararg. Trong trường hợp phổ biến như vậy, applysẽ chỉ thêm một chút chi phí. (Lưu ý không có gì phải thực sự lo lắng.)

Mặt khác, một chức năng phức tạp có thể tận dụng một số cơ hội tối ưu hóa mà không đủ chung để tích hợp vào reduce; sau đó applysẽ cho phép bạn tận dụng những lợi ích đó trong khi reducethực sự có thể làm bạn chậm lại. Một ví dụ điển hình của kịch bản sau xảy ra trong thực tế được cung cấp bởi str: nó sử dụng StringBuildernội bộ và sẽ được hưởng lợi đáng kể từ việc sử dụng applychứ không phải reduce.

Vì vậy, tôi muốn sử dụng applykhi nghi ngờ; và nếu bạn tình cờ biết rằng nó sẽ không mua cho bạn bất cứ thứ gì reduce(và điều này sẽ không thể thay đổi sớm), hãy thoải mái sử dụng reduceđể loại bỏ chi phí không cần thiết đó nếu bạn cảm thấy như vậy.


Câu trả lời chính xác. Bên cạnh đó, tại sao không bao gồm sumchức năng tích hợp như trong haskell? Có vẻ như một hoạt động khá phổ biến.
dbyrne

17
Cảm ơn, rất vui khi nghe điều đó! Re : sum, Tôi muốn nói rằng Clojure có chức năng này, nó được gọi +và bạn có thể sử dụng nó với apply. :-) Nói nghiêm túc, tôi nghĩ rằng trong Lisp, nói chung, nếu một chức năng variadic được cung cấp, nó thường không kèm theo một điều hành wrapper trên bộ sưu tập - đó là những gì bạn sử dụng applycho (hoặc reduce, nếu bạn biết rằng ý nghĩa hơn).
Michał Marc:

6
Thật buồn cười, lời khuyên của tôi thì ngược lại: reducekhi nghi ngờ, applykhi bạn biết chắc chắn có sự tối ưu hóa. reduceHợp đồng chính xác hơn và do đó dễ bị tối ưu hóa chung hơn. applylà mơ hồ hơn và do đó chỉ có thể được tối ưu hóa trên cơ sở từng trường hợp. strconcatlà hai ngoại lệ phổ biến.
cgrand

1
@cgrand Việc chia sẻ lại lý do của tôi có thể đại khái là cho các hàm trong đó reduceapplytương đương về mặt kết quả, tôi hy vọng tác giả của hàm được đề cập sẽ biết cách tốt nhất để tối ưu hóa quá tải biến đổi của chúng và chỉ thực hiện theo cách reducenếu đó thực sự là những gì có ý nghĩa nhất (tùy chọn để làm như vậy chắc chắn luôn luôn có sẵn và làm cho một mặc định rõ ràng hợp lý). Tuy nhiên, tôi thấy bạn đến từ đâu, reducechắc chắn là trung tâm của câu chuyện hiệu suất của Clojure (và ngày càng như vậy), được tối ưu hóa rất cao và được chỉ định rất rõ ràng.
Michał Marc:

51

Đối với những người mới nhìn vào câu trả lời này,
hãy cẩn thận, chúng không giống nhau:

(apply hash-map [:a 5 :b 6])
;= {:a 5, :b 6}
(reduce hash-map [:a 5 :b 6])
;= {{{:a 5} :b} 6}

21

Ý kiến ​​khác nhau - Trong thế giới Lisp rộng lớn hơn, reducechắc chắn được coi là thành ngữ hơn. Đầu tiên, có những vấn đề mang tính đột biến đã được thảo luận. Ngoài ra, một số trình biên dịch Lisp thông thường sẽ thực sự thất bại khi applyđược áp dụng cho các danh sách rất dài do cách chúng xử lý danh sách đối số.

Tuy nhiên, applytrong số các Clojurists trong vòng tròn của tôi, sử dụng trong trường hợp này có vẻ phổ biến hơn. Tôi thấy nó dễ dàng hơn để mò mẫm và cũng thích nó.


19

Nó không tạo ra sự khác biệt trong trường hợp này, bởi vì + là trường hợp đặc biệt có thể áp dụng cho bất kỳ số lượng đối số nào. Giảm là một cách để áp dụng một hàm mong đợi một số lượng đối số cố định (2) vào một danh sách các đối số dài tùy ý.


9

Tôi thường thấy mình thích giảm bớt khi hành động trên bất kỳ loại bộ sưu tập nào - nó hoạt động tốt và nói chung là một chức năng khá hữu ích.

Lý do chính tôi sẽ sử dụng áp dụng là nếu các tham số có nghĩa là những thứ khác nhau ở các vị trí khác nhau hoặc nếu bạn có một vài tham số ban đầu nhưng muốn lấy phần còn lại từ bộ sưu tập, ví dụ:

(apply + 1 2 other-number-list)

9

Trong trường hợp cụ thể này tôi thích reducevì nó dễ đọc hơn : khi tôi đọc

(reduce + some-numbers)

Tôi biết ngay rằng bạn đang biến một chuỗi thành một giá trị.

Với applytôi phải xem xét chức năng nào đang được áp dụng: "ah, đó là +chức năng, vì vậy tôi nhận được ... một số duy nhất". Hơi ít đơn giản.


7

Khi sử dụng một chức năng đơn giản như +, bạn thực sự không sử dụng chức năng nào.

Nói chung, ý tưởng reducelà một hoạt động tích lũy. Bạn trình bày giá trị tích lũy hiện tại và một giá trị mới cho hàm tích lũy của bạn Kết quả của hàm là giá trị tích lũy cho lần lặp tiếp theo. Vì vậy, các lần lặp của bạn trông giống như:

cum-val[i+1] = F( cum-val[i], input-val[i] )    ; please forgive the java-like syntax!

Để áp dụng, ý tưởng là bạn đang cố gắng gọi một hàm mong đợi một số đối số vô hướng, nhưng chúng hiện đang nằm trong một bộ sưu tập và cần phải được rút ra. Vì vậy, thay vì nói:

vals = [ val1 val2 val3 ]
(some-fn (vals 0) (vals 1) (vals 2))

chúng ta có thể nói:

(apply some-fn vals)

và nó được chuyển đổi tương đương với:

(some-fn val1 val2 val3)

Vì vậy, sử dụng "áp dụng" giống như "xóa dấu ngoặc đơn" xung quanh chuỗi.


4

Hơi muộn về chủ đề này nhưng tôi đã làm một thí nghiệm đơn giản sau khi đọc ví dụ này. Đây là kết quả từ sự thay thế của tôi, tôi chỉ không thể suy ra bất cứ điều gì từ phản hồi, nhưng dường như có một vài cú đá bộ đệm giữa giảm và áp dụng.

user=> (time (reduce + (range 1e3)))
"Elapsed time: 5.543 msecs"
499500
user=> (time (apply + (range 1e3))) 
"Elapsed time: 5.263 msecs"
499500
user=> (time (apply + (range 1e4)))
"Elapsed time: 19.721 msecs"
49995000
user=> (time (reduce + (range 1e4)))
"Elapsed time: 1.409 msecs"
49995000
user=> (time (reduce + (range 1e5)))
"Elapsed time: 17.524 msecs"
4999950000
user=> (time (apply + (range 1e5)))
"Elapsed time: 11.548 msecs"
4999950000

Nhìn vào mã nguồn của clojure làm giảm đệ quy khá sạch với giảm nội bộ, không tìm thấy bất cứ điều gì khi triển khai áp dụng. Việc thực hiện Clojure của + để áp dụng giảm nội bộ, được lưu trong bộ nhớ cache bằng cách thay thế, dường như giải thích cuộc gọi thứ 4. Ai đó có thể làm rõ những gì thực sự xảy ra ở đây?


Tôi biết tôi sẽ thích giảm bất cứ khi nào tôi có thể :)
rohit

2
Bạn không nên thực hiện rangecuộc gọi bên trong timebiểu mẫu. Đặt nó bên ngoài để loại bỏ sự can thiệp của xây dựng trình tự. Trong trường hợp của tôi, reduceluôn luôn tốt hơn apply.
Davyzhu

3

Vẻ đẹp của ứng dụng là hàm đã cho (+ trong trường hợp này) có thể được áp dụng cho danh sách đối số được hình thành bởi các đối số can thiệp trước khi chờ xử lý với một bộ sưu tập kết thúc. Giảm là một sự trừu tượng để xử lý các mục bộ sưu tập áp dụng hàm cho từng mục và không hoạt động với trường hợp đối số biến.

(apply + 1 2 3 [3 4])
=> 13
(reduce + 1 2 3 [3 4])
ArityException Wrong number of args (5) passed to: core/reduce  clojure.lang.AFn.throwArity (AFn.java:429)
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.