Sức mạnh tuyện vời của Inject
Bài đăng này đã không được cập nhật trong 8 năm
Inject là một nền móng để xây dựng block
Inject là một trong những cấu trúc cơ bản, linh hoạt nhất có trong các ngôn ngữ hướng chức năng. Nó có thể được sử dụng để xây dựng map, select, max, all? và nhiều phương thức sử dụng vòng lặp khác. Thật không may, nhiều lập trình viên lại không hiểu rõ sức mạnh tuyệt vời của nó. Bài viết này sẽ giúp các bạn hiểu rõ về sức mạnh của nó.
Hãy cùng tìm hiểu
Đầu tiên, hãy xem ví dụ tính tổng các số trong một mảng dưới đây:
def sum(numbers)
result = 0
numbers.each do |number|
result += number
end
result
end
sum([1, 3, 5, 7, 9]) # => 25
Bây giờ, chúng ta hãy cố gắng giải quyết bài toán trên với một yêu cầu đơn giản. Hãy tính toán kết quả mà không thay đổi các giá trị của các biến, như trong ví dụ sau:
result += number
Đây là một bước rất quan trọng để làm cho mã của chúng ta thêm linh hoạt hơn. Thay đổi giá trị của biến, sẽ ngăn chặn mã của chúng ta có thể chạy trên nhiều bộ xử lý, đồng thời cũng gây khó khăn cho việc tìm lỗi.
Để giải quyết yêu cầu trên, chúng ta sử dụng đệ quy. Hãy viết lại đoạn mã trên sử dụng đệ quy.
def sum(numbers)
if numbers.empty?
else
first, *rest = numbers
first + sum(rest)
end
end
sum([1, 3, 5, 7, 9]) # => 25
Accumulators (Ác quy)
Để tạo ra một thuật toán linh hoạt hơn, và để tối ưu hóa nó cho việc sử dụng đệ quy, chúng ta sử dụng một giá trị khởi đầu, và sẽ gọi nó là accumulator
.
def sum(accumulator, numbers)
if numbers.empty?
accumulator
else
first, *rest = numbers
sum(accumulator + first, rest)
end
end
sum(20, [1, 3, 5, 7, 9]) # => 45
Tên accumulator
có thể gây nhầm lẫn, nhưng bạn có thể hiểu nó như là một biến mà tích lũy kết quả. Mục đích của nó là tương tự với biến rusult
trong ví dụ ban đầu của chúng ta.
Giới thiệu về inject
Khi đã hiểu về cấu trúc đệ quy trong ví dụ trên, chúng ta hãy cùng tìm hiểu về phương thức inject
.
Áp dụng cho bài toán tính tổng một mảng:
[1, 3, 5, 7, 9].inject(20) { |result, number| result + number } # => 45
Áp dụng với bài toán nhân các phần tử của một mảng:
[1, 3, 5, 7, 9].inject(2) { |result, number| result * number } # => 805
Thật tiện dụng khi chúng ta muốn chuyển đổi một mảng các giá trị vào một giá trị. Đây là một trong những ưu điểm chính của phương thức inject
. Trong ví dụ này chúng tôi đang giảm mảng thành một giá trị duy nhất. Đây là lý do tại sao các phương thức inject
cũng thường được đặt tên là reduce
.
Tối giản phương thức inject
{ |result, number| result + number }
được sử dụng cho hai ví dụ trên.
May mắn, Ruby là một ngôn ngữ mạnh mẽ cho phép chúng ta viết những dòng trên thậm chí còn ngắn hơn. Sử dụng :*
là một cách viết tắt cho một block
mà sẽ nhân với từng giá trị trong mỗi vòng lặp.
[1, 3, 5, 7, 9].inject(1, :*) # => 945
Ta có thể định nghĩa lại hai phương thức tính tổng và nhân các phần tử của mảng như sau:
def sum(elements, from = 0)
elements.inject(from, :+)
end
def product(elements, from = 1)
elements.inject(from, :*)
end
Xây dựng phương thức map
May mắn thay, inject
có thể làm nhiều hơn nữa. Phương thức map
rất mạnh mẽ như một dạng đặc biệt của phương thức inject
. Hai ví dụ dưới đây cùng có một chức năng.
[1, 2, 3].map {|el| el * el}
[1, 2, 3].inject([]) {|result, el| result + [el * el]}
Chúng ta có thể định nghĩa phương thức map sử dụng inject
def map(elements, &block)
elements.inject([]) {|result, el| result + [block.call(el)]}
end
Xây dựng phương thức select
Chúng ta có thể định nghĩa phương thức select
sử dụng inject
như sau:
def select(elements, &block)
elements.inject([]) {|result, el| result + (block.call(el) ? [el] : [])}
end
Tương tự với phương thức reject
def reject(elements, &block)
elements - select(elements, &block)
end
Xây dựng phương thức min
và max
Chúng ta có thể định nghĩa phương thức min
để tìm giá trị nhỏ nhất của một mảng:
def min(elements, &block)
elements.inject(Float::INFINITY) do |minimum, el|
el < minimum ? el : minimum
end
end
Tương tự với phương thức tìm giá trị lớn nhất max
def max(elements, &block)
elements.inject(-Float::INFINITY) do |maximum, el|
el > minimum ? el : maximum
end
end
Xây dựng phương thức all?
Phương thức all?
để kiểm tra nếu mọi phần tử trong mảng thỏa mãn điều kiện đầu vào.
def all?(elements, &block)
elements.inject(true) { |result, el| result && block.call(el) }
end
Tương tự với phương thức any?
def any?(elements, &block)
elements.inject(false) { |result, el| result || block.call(el) }
end
Có nên sử dụng inject
để viết code?
Thay vì sử dụng inject
ở khắp mọi nơi, sử dụng nó để xây dựng phương thức mới. Ví dụ, thay vì viết:
usernames = ["tim", "jake", "jennifer", "marcus"]
usernames.inject({}) do |result, username|
result.merge(username => username.length)
end
# => { "tim" => 3, "jake" => 4, "jennifer" => 8, "marcus" => 6 }
Ta có thể xây dựng một phương thức như sau:
def hash_map(elements, &block)
elements.inject({}) do |map, el|
map.merge(el => block.call(el))
end
end
Sau đó sử dụng nó vô cùng đơn giản
usernames = ["tim", "jake", "jennifer", "marcus"]
hash_map(usernames) { |username| username.length }
# => { "tim" => 3, "jake" => 4, "jennifer" => 8, "marcus" => 6 }
Hoặc ngắn hơn nữa
hash_map(usernames, &:length)
Kết luận
inject
là siêu tuyệt vời, và đủ mạnh mẽ để thực hiện các bài toán sử dụng vòng lặp.
Nguồn tham khảo
http://renderedtext.com/blog/2016/02/18/inject-is-a-fundamental-building-block/
All rights reserved