2015-02-19 15 views
12

Một thời gian trước, tôi đã đăng câu hỏi trên StackOverflow cho biết rằng native implementation of reduceRight in JavaScript is annoying. Do đó, tôi tạo ra một hàm Haskell kiểu foldr như một phương thuốc:Ví dụ thế giới thực về cách sử dụng reduceRight trong JavaScript

function foldr(array, callback, initial) { 
    var length = array.length; 

    if (arguments.length < 3) { 
     if (length > 0) var result = array[--length]; 
     else throw new Error("Reduce of empty array with no initial value"); 
    } else var result = initial; 

    while (length > 0) { 
     var index = --length; 
     result = callback(array[index], result, index, array); 
    } 

    return result; 
} 

Tuy nhiên, tôi không bao giờ sử dụng foldr chức năng này đơn giản chỉ vì tôi không bao giờ cần thiết để lặp qua một mảng từ sang trái phải. Điều này khiến tôi suy nghĩ, tại sao tôi không sử dụng foldr trong JavaScript nhiều như tôi làm trong Haskell và một số ví dụ thực tế về việc sử dụng foldr trong JavaScript là gì?

Tôi có thể sai, nhưng tôi tin rằng foldr chức năng được sử dụng rộng rãi trong Haskell vì:

  1. Lazy đánh giá (foldl is tail recursive, so how come foldr runs faster than foldl?)
  2. ngắn cắt hợp sử dụng foldr/build (Correctness of short cut fusion: foldr/build)

Điều này sẽ giải thích tại sao foldr hoặc reduceRight không được sử dụng rộng rãi trong JavaScript. Tôi chưa thấy việc sử dụng thế giới thực của foldr chỉ cho thứ tự lặp lại từ phải sang trái của nó.

này mang lại cho tôi hai câu hỏi của tôi:

  1. một số ví dụ thế giới thực của việc sử dụng reduceRight trong JavaScript là gì? Có lẽ bạn đã sử dụng nó trong một gói npm. Sẽ thật tuyệt nếu bạn có thể liên kết tôi với mã của bạn và giải thích lý do bạn cần sử dụng reduceRight thay vì reduce.
  2. Tại sao reduceRight không được sử dụng rộng rãi như reduce bằng JavaScript? Tôi đã cung cấp hai xu cho vấn đề này. Tôi tin rằng foldr chủ yếu chỉ được sử dụng cho sự lười biếng của nó, đó là lý do tại sao reduceRight không phải là rất hữu ích trong JavaScript. Tuy nhiên, tôi có thể sai.

Đối với câu hỏi đầu tiên, tôi đã cố gắng tìm một số ví dụ thực tế về sử dụng reduceRight trong JavaScript. Tuy nhiên, tôi không tìm thấy bất kỳ câu trả lời thỏa đáng nào. Các ví dụ duy nhất tôi thấy là tầm thường và lý thuyết:

when to use reduce and reduceRight?

Những gì tôi đang tìm kiếm là một ví dụ thực tế. Khi nào thực tế sử dụng reduceRight trong JavaScript thay vì reduce?

Đối với câu hỏi thứ hai, tôi hiểu rằng đó chủ yếu là ý kiến ​​dựa trên đó là lý do tại sao nó không sao nếu bạn không trả lời nó. Trọng tâm chính của bài đăng này là câu hỏi đầu tiên, không phải là câu hỏi thứ hai.

+0

Tôi không nghĩ chức năng 'foldr' trong các lần truyền tải haskell từ phải sang trái? – Sibi

+1

@Sibi Bạn nói đúng. Không thể duyệt qua danh sách từ phải sang trái trong Haskell do cách xác định danh sách (tức là 'dữ liệu [a] = [] | a: [a]'). Hàm 'foldr' trong Haskell chỉ tích lũy kết quả từ phải sang trái. Tuy nhiên, câu hỏi này thực sự không phải là về Haskell. –

+1

Tôi có nghĩa đen không bao giờ sử dụng 'reduceRight' trong mã. Tôi đã sử dụng 'reduce' ít nhất một ngàn lần. –

Trả lời

7

Để trả lời câu hỏi đầu tiên, reduceRight có ích khi bạn muốn chỉ định các mục theo cách từ trái sang phải nhưng thực thi chúng theo cách từ phải sang trái.

xem xét thực hiện ngây thơ này của một hàm soạn thảo để lấy đối số từ trái sang phải nhưng được đọc và thực hiện từ phải sang trái:

var compose = function() { 
    var args = [].slice.call(arguments); 

    return function (initial) { 
     return args.reduceRight(function (prev, next) { 
      return next(prev); 
     }, initial); 
    } 
} 

Thay vì mất thời gian/không gian với một cuộc gọi đến reverse trên mảng, việc gọi điện reduceRight trở nên đơn giản và dễ dàng hơn.

Một ví dụ của việc sử dụng compose chức năng này sẽ trông giống như sau:

var square = function (input) { 
    return input * input; 
}; 

var add5 = function (input) { 
    return input + 5; 
}; 

var log = function (input) { 
    console.log(input); 
}; 

var result = compose(log, square, add5)(1); // -> 36 

Tôi chắc chắn có rất nhiều ví dụ về kỹ thuật hơn reduceRight là hữu ích và điều này chỉ có một.

+0

Thật vậy, thành phần hàm đọc từ phải sang trái và do đó nó có ý nghĩa để sử dụng 'reduceRight' trong trường hợp đó. Tuy nhiên, bạn có thể dễ dàng tạo thành phần hàm đọc từ trái sang phải và sử dụng 'reduce' để thay thế. Tôi đoán rằng đó chỉ là vấn đề ưu tiên. Cá nhân, nếu tôi thực hiện hàm 'compose' thì tôi sẽ không sử dụng' reduce' cũng như 'reduceRight' vì lý do hiệu suất. Tôi sẽ không muốn [hai lần số lượng chi phí cuộc gọi chức năng] (http://jsperf.com/compose-fold-vs-direct) cho một hàm tổng hợp. Như bạn đã nói, có thể có những ví dụ tốt hơn. Do đó tôi sẽ đợi nó. –

+2

@ [Jamie Dixon] bạn đúng, đó là một tiện ích tiện lợi hoạt động trong proxy của Array.reverse –

4

Bạn hoàn toàn chính xác, đến mức tôi không hoàn toàn chắc chắn đây thậm chí là một câu hỏi thực sự. Sự lười biếng và nhiệt hạch là cả hai lớn lý do tại sao foldr được ưu tiên hơn trong Haskell. Cả hai điều này đều không có trong các mảng của JavaScript, do đó, không có lý do gì để sử dụng reduceRight trong JavaScript thực tế. Ý tôi là, bạn có thể tạo ra một trường hợp mà bạn đã xây dựng một mảng bằng cách đẩy mọi thứ vào cuối, và sau đó bạn muốn lặp lại chúng từ mới đến cũ nhất trong khi tích lũy một kết quả. Nhưng imo đó là rất contrived.


Chỉ để minh họa mặt bên của Haskell. Lưu ý rằng trong Haskell, một lần phải không không thực sự thực hiện đánh giá từ phải sang trái. Bạn có thể suy nghĩ về việc nhóm đánh giá từ phải sang trái, nhưng do lười biếng, đó không phải là số được tính. Hãy xem xét:

foldr (\a _ -> Just a) undefined [1..] 

Tôi đã cung cấp giá trị bắt đầu cho số tiền tích lũy undefined và danh sách số tự nhiên vô hạn để gấp. Ôi trời ạ. Tuy nhiên, vấn đề này không phải là một chút. Biểu thức này vui vẻ đánh giá thành Just 1.

Về mặt lý thuyết, các nhóm làm việc như thế này:

let step a _ = Just a 
let foldTheRest = foldr step undefined [2..] 
step 1 foldTheRest 

Suy nghĩ về việc lập nhóm, chúng ta "gấp phần còn lại", sau đó chúng tôi áp dụng step chức năng để hai đối số: "Yếu tố đầu tiên của danh sách", và "bất cứ điều gì chúng tôi nhận được từ việc gấp phần còn lại của danh sách." Tuy nhiên, kể từ khi chức năng bước thậm chí không cần đối số tích lũy của nó, một phần của tính toán không bao giờ được đánh giá. Tất cả những gì chúng tôi cần để đánh giá nếp gấp đặc biệt này là "yếu tố đầu tiên của danh sách".


Nói cách khác, các mảng JavaScript giữ none trong những lợi ích của foldr rằng Haskell thích, vì vậy có nghĩa là không có lý do để sử dụng reduceRight. (Ngược lại, đôi khi có lý do chính đáng trong Haskell để sử dụng một nếp gấp bên trái nghiêm ngặt.)

n.b. Tôi không đồng ý với câu hỏi khác của bạn, nơi bạn kết luận rằng "việc triển khai bản địa của reduceRight là sai". Tôi đồng ý rằng đó là gây phiền nhiễu rằng họ đã chọn thứ tự đối số mà họ đã thực hiện, nhưng không phải là sai.

2

Sử dụng reduceRight khi bạn đang cố gắng để tạo ra một mảng mới các hạng mục đuôi từ một mảng nhất định:

var arr = [1,2,2,32,23,4,5,66,22,35,78,8,9,9,4,21,1,1,3,4,4,64,46,46,46,4,6,467,3,67]; 

function tailItems(num) { 
    var num = num + 1 || 1; 
    return arr.reduceRight(function(accumulator, curr, idx, arr) { 
     if (idx > arr.length - num) { 
      accumulator.push(curr); 
     } 
     return accumulator; 
    }, []); 
} 

console.log(tailItems(5)); 

//=> [67, 3, 467, 6, 4] 

Một điều thú vị đó là hữu ích về reduceRight là vì nó đảo ngược mảng mà nó được biểu diễn trên , tham số chỉ số của hàm chiếu cung cấp chỉ số mảng bắt đầu từ arr.length, chẳng hạn như:

var arr = [1,2,2,32,23]; 

arr.reduceRight(function(accumulator, curr, idx, arr) { 
    console.log(idx); 
}, ''); 

// => 4,3,2,1,0 

Điều này có thể hữu ích nếu bạn đang cố gắng để tìm một mục mảng cụ thể bằng cách chỉ số của nó tức là arr[idx] mà là về phía đuôi của một mảng, mà rõ ràng là trở nên thực tế hơn các mảng lớn hơn - nghĩ rằng hàng ngàn mặt hàng.

Có lẽ tốt trong ví dụ hoang dã sẽ là nguồn cấp dữ liệu truyền thông xã hội hiển thị 10 mục mới nhất lúc đầu và có thể tải nhiều hơn theo yêu cầu. Hãy xem xét cách reduceRight có thể thực tế trong trường hợp này để tổ chức các bộ sưu tập các mảng theo thứ tự ngược lại để phù hợp với ví dụ này.

+0

Ví dụ hay, Elise. –

+0

Đây là cả hai ví dụ xấu, vì cả hai đều lạm dụng thao tác gấp: đầu tiên, bạn đang thay đổi một giá trị 'accumulator' duy nhất và trong lần thứ hai bạn đang in nội dung. Điều đó có nghĩa là cả hai trường hợp không thực sự * gấp *, chúng chỉ là lặp lại. –

1

Ngoài ra, bạn có thể sử dụng reduceRight, nếu bạn cần xây dựng cấu trúc tính toán/dữ liệu lồng nhau trong ra ngoài. Thay vì tự viết

const compk = f => g => x => k => f(x) (x => g(x) (k)); 
 

 
const compk3 = (f, g, h) => x => k => f(x) (x => g(x) (y => h(y) (k))); 
 

 
const inck = x => k => setTimeout(k, 0, x + 1); 
 

 
const log = prefix => x => console.log(prefix, x); 
 

 
compk3(inck, inck, inck) (0) (log("manually")); // 3

Tôi muốn áp dụng một giải pháp theo chương trình đó xây dựng cấu trúc đệ quy:

const compkn = (...fs) => k => fs.reduceRight((chain, f) => x => f(x) (chain), k); 
 

 
const inc = x => x + 1; 
 

 
const lift = f => x => k => k(f(x)); 
 

 
const inck = x => k => setTimeout(k, 0, x + 1); 
 

 
const log = prefix => x => console.log(prefix, x); 
 

 
compkn(inck, lift(inc), inck) (log("programmatically")) (0); // 0

0

Array.reduceRight() là rất tốt khi:

  • bạn cần để lặp qua một mảng các mặt hàng để tạo ra HTML
  • VÀ cần một bộ đếm trong HTML trước đến các mục

.

var bands = { 
    Beatles: [ 
     {name: "John", instruments: "Guitar"}, 
     {name: "Paul", instruments: "Guitar"}, 
     {name: "George", instruments: "Guitar"}, 
     {name: "Ringo", instruments: "Drums"}] 
}; 
function listBandplayers(bandname, instrument) { 
    var bandmembers = bands[bandname]; 
    var arr = [ "<B>" , 0 , ` of ${bandmembers.length} ${bandname} play ` , instrument , "</B>", 
       "\n<UL>" , ...bandmembers , "\n</UL>" ]; 
    var countidx = 1; 
    return arr.reduceRight((html, item, idx, _array) => { 
      if (typeof item === 'object') { 
       if (item.instruments.contains(instrument)) _array[countidx]++; 
       item = `\n\t<LI data-instruments="${item.instruments}">` + item.name + "</LI>"; 
      } 
      return item + html; 
    }); 
} 
console.log(listBandplayers('Beatles', 'Drums')); 
/* 
<B>1 of 4 Beatles play Drums</B> 
<UL> 
    <LI data-instruments="Guitar">John</LI> 
    <LI data-instruments="Guitar">Paul</LI> 
    <LI data-instruments="Guitar">George</LI> 
    <LI data-instruments="Drums">Ringo</LI> 
</UL> 
*/ 
console.log(listBandplayers('Beatles', 'Guitar')); 
/* 
<B>3 of 4 Beatles play Guitar</B> 
<UL> 
    <LI data-instruments="Guitar">John</LI> 
    <LI data-instruments="Guitar">Paul</LI> 
    <LI data-instruments="Guitar">George</LI> 
    <LI data-instruments="Drums">Ringo</LI> 
</UL> 
*/ 
0

Array.prototype.reduceRight thực sự hữu ích trong Javascript ngay cả với đánh giá nghiêm ngặt. Để xem lý do tại sao chúng ta hãy nhìn vào bộ chuyển đổi bản đồ trong Javascript và một ví dụ tầm thường. Kể từ khi tôi là một lập trình viên chức năng phiên bản của tôi chủ yếu dựa vào chức năng cà ri:

const mapper = f => g => x => y => g(x) (f(y)); 
 

 
const foldl = f => acc => xs => xs.reduce((acc, x) => f(acc) (x), acc); 
 

 
const add = x => y => x + y; 
 

 
const get = k => o => o[k]; 
 

 
const len = get("length"); 
 

 
const concatLen = foldl(mapper(len) (add)) (0); 
 

 
const xss = [[1], [1,2], [1,2,3]]; 
 

 
console.log(concatLen(xss)); // 6

mapper (f => g => x => y => g(x) (f(y))) là chủ yếu hoạt động thành phần trong đối số thứ hai. Và khi chúng ta nhớ lại rằng một nếp gấp bên phải ((a -> b -> b) -> b -> [a] -> b) đã lật đối số bên trong bộ giảm tốc, chúng ta có thể thay thế mapper bằng thành phần chức năng bình thường. Do trừu tượng hóa trên tinh thần, chúng ta cũng có thể soạn một hàm nhị phân với một hàm đơn nhất một cách dễ dàng.

Thật không may, thứ tự đối số của Array.prototype.reducdeRight bị hỏng, vì nó hy vọng bộ tích lũy làm đối số đầu tiên của bộ giảm tốc.Vì vậy, chúng ta cần phải khắc phục điều đó với một tìm lambda lạ:

const foldr = f => acc => xs => xs.reduceRight((acc, x) => f(x) (acc), acc); 
 

 
const comp = (f, g) => x => f(g(x)); 
 

 
const add = x => y => x + y; 
 

 
const len = xs => xs.length; 
 

 
const concatLen = foldr(comp(add, len)) (0); 
 

 
const xss = [[1], [1,2], [1,2,3]]; 
 

 
console.log(concatLen(xss));

Tôi nghĩ rằng đây là một sự cải thiện đáng kể từ khi chúng tôi giảm bớt gánh nặng về tinh thần để thực sự hiểu mã (comp là phổ biến hơn mapper) và thậm chí còn DRY hơn.

Các vấn đề liên quan