+3

Sức mạnh tuyện vời của Inject

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 minmax

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

Viblo
Let's register a Viblo Account to get more interesting posts.