Reduce trong Ruby

Reduce là một function của Enumerable, tuy nhiên với nhiều Rubyists function này rất ít khi được sử dụng. Mọi người thường sử dụng reduce khi muốn tính tổng.

[1, 2, 3].reduce :+

Nhưng từ Ruby 2.4.x trở lên thì chúng ta đã có sum làm việc đó

[1, 2, 3].sum

Nếu vậy chẳng nhẽ reduce trở lên vô dụng rồi sao ? Không phải vậy, bí mật của reduceMọi chức năng Enumerable có thể được cài đặt trong giới hạn của reduce

Trong bài viết này tôi sẽ trình bày về cách thức sử dụng của reduce và một số hàm khác tương tự trong ruby.

Map

Map được sử dụng cho để áp dụng một function cho một tập các Enumerable.

[1,2,3].map { |i| i * 2 }
# => [2,4,6]

Lưu ý: map không làm thay đổi array gốc, mà chỉ return về một mảng mới đã được biến đổi theo function.

Select

Giống như map, ngoại trừ việc function của nó như một điều kiện, nếu điều kiện function đúng thì nó sẽ giữ nguyên element cho danh sách mới, nếu không thì loại bỏ nó đi

[1,2,3].select { |i| i > 1 }
# => [2, 3]

Reduce

Từ đây, tôi sẽ phân tích chi tiết cách hoạt động của reduce Ví dụ, chúng ta phân tích đoạn code sau đây:

[1,2,3].reduce(0) { |accumulator, i| accumulator + i }
# => 6

[1,2,3].reduce(0) đầu tiên gán giá trị khởi tạo là 0 { |accumulator, i| accumulator + i } truyền vào 2 tham số accumulatori. Giá trị đầu tiên của accumulator sẽ là giá trị khởi tạo ban đầu hoặc phần tử đầu tiên của list. Với mỗi chu kỳ, accumulator được gán giá trị là giá trị trả về của chu kỳ cuối.

 a | i | reducer
---+---+-----------
 0 | 1 | 0 + 1 : 1
 1 | 2 | 1 + 2 : 3
 3 | 3 | 3 + 3 : 6
 6 | - |     -
---+---+-----------

Returns: 6

Về cơ bản, trống nó giống như thế này ((((0) + 1) + 2) + 3)

Map bằng reduce

Reduce không quan tâm tới giá trị khởi tạo ban đầu là loại gì ? trong ví dụ trước chúng ta dùng giá trị khởi tạo là integer, nhưng chúng ta hoàn toàn có thể sử dụng data type bất kỳ nhưng array, string... Với reduce, chúng ta có thể tạo ra một map bằng cách sử dụng giá trị khởi tạo là array. Ví dụ:

def map(list, &fn)
  list.reduce([]) { |a, i| a.push(fn[i]) }
end
map([1,2,3]) { |i| i * 2 }
# => [2, 4, 6]

Đây là cách hoạt động của function map tạo bằng reduce

    a   | i | fn[i]     | reduction         
--------+---+-----------+---------------
 []     | 1 | 1 * 2 : 2 | [].push(2)    
 [2]    | 2 | 2 * 2 : 4 | [2].push(4)   
 [2,4]  | 3 | 3 * 2 : 6 | [2,4].push(6) 
 [2,4,6]| - |     -     |       -          
--------+---+-----------+----------------
Returns: [2, 4, 6]

Select bằng reduce

Tương tự map, cũng khá dễ dàng để tạo select

def select(list, &fn)
  list.reduce([]) { |a, i| fn[i] ? a.push(i) : a }
end
select([1,2,3]) { |i| i > 1 }
# => [2, 3]

Đây là cách hoạt động của select tạo bằng reduce

    a  | i | fn[i]         | reduction                 
-------+---+---------------+---------------------------
 []    | 1 | 1 > 1 : false | false ? [].push(i)  : []  
 []    | 2 | 2 > 1 : true  | true  ? [].push(i)  : []  
 [2]   | 3 | 3 > 1 : true  | true  ? [2].push(i) : [2] 
 [2,3] | - |       -       |             -             
-------+---+---------------+---------------------------
Returns: [2, 3]

Find bằng reduce

Trong trường hợp của find, ta chỉ muốn trả về một giá trị tương ứng với điều kiện thích hợp, vì vậy ta không cần duyệt hết list, mà chỉ cần duyệt đến khi nào tìm thấy giá trị thích hợp. Ta có thể sử dụng break trong trường hợp này.

def find(list, &fn)
  list.reduce(nil) { |_, i| break i if fn[i] }
end
find([1,2,3]) { |i| i == 2 }
# => 2
find([1,2,3]) { |i| i == 4 }
# => nil

Cách thức haojt động của find tạo bằng reduce

  a  | i | fn[i]          | reduction        
-----+---+----------------+------------------
 nil | 1 | 1 == 2 : false | break i if false 
 nil | 2 | 2 == 2 : true  | break i if true  
-----+---+----------------+------------------
Break: 2
  a  | i | fn[i]          | reduction        
-----+---+----------------+------------------
 nil | 1 | 1 == 4 : false | break i if false 
 nil | 2 | 2 == 4 : false | break i if false 
 nil | 3 | 3 == 4 : false | break i if false 
 nil | - |        -       |         -        
-----+---+----------------+------------------
Returns: nil

Combining Functions

Có thể nói, với reduce, chúng ta có thể xây dựng được chức năng của map, select, find và nó còn có thể làm được với nhiều function khác nữa. Chẳng hạn việc kết hợp các function với nhau kiểu map_compact hoặc map_select...

Ví dụ hàm map_compact:

def map_compact(list, &fn)
  list.reduce([]) { |a, i|
    value = fn[i]
    value.nil? ? a : a.push(value)
  }
end
map_compact([1,2,3]) { |i| i > 1 ? i * 2 : nil }
# => [4, 6]

Nó giống với select, nhưng nó có thể return một list các phần tử được lọc đồng thời tùy biến lại các phần tử đó.

    a  | i | fn[i]               | reduction                     
-------+---+---------------------+-------------------------------
 []    | 1 | 1 > 1 : nil         | nil.nil? ? []  : [].push(1)   
 []    | 2 | 2 > 1 : 2 * 2 : 4   | 4.nil?   ? []  : [].push(4)   
 [4]   | 3 | 3 > 1 : 3 * 2 : 6   | 6.nil?   ? [4] : [4].push(6)  
 [4,6] | - |          -          |               -                 
-------+---+---------------------+-------------------------------
Returns: [4, 6]

Tham khảo

https://medium.com/@baweaver/reducing-enumerable-the-basics-fa042ce6806