Một số hàm xử lí object trong ruby bạn nên biết.

Xin chào các bạn, ruby là một ngôn ngữ hỗ trợ rất nhiều các hàm dựng sẵn hay bên trong các class, trong quá trình làm việc đôi khi chúng ta cần các hàm với chức năng tương tự nhưng lại không biết dẫn đến tốn nhiều thời gian cho việc code lại hàm mới, bài viết này mình sẽ tổng hợp một số hàm xử lí trong ruby với các ví dụ cụ thể. Hy vọng bài viết sẽ giúp ích cho các bạn trong việc tối ưu và làm đẹp code.

1. Object#tap Có khi nào bạn gặp một tình huống cần xử lí các thông tin bên trong 1 đối tượng và muốn thay đổi chính đối tượng đó, thay vì trả về một đối tượng mới không?. Ví dụ bạn có 1 Hash, sau khi xử lí các value của hash, bạn phải thêm 1 dòng trả về chính hash đó. Cách viết này nhìn có vẻ không đẹp mắt và code không được tối ưu lắm, ví dụ như sau:

def update_params(params)
  params[:foo] = 'bar'
  params
end

Bạn truyển params là 1 hash vào trong hàm, sau khi gán key "foo" bằng value "bar" ta phải trả về params. Thay vì như thế, chúng ta có thể dùng hàm tap, truyền vào 1 block và object đó sẽ tự động được trả về như sau:

def update_params(params)
  params.tap {|p| p[:foo] = 'bar' }
end

#tap là 1 hàm có mặt trong rất nhiều Class, bạn có thể sử dụng ở các class khác với mục đích tương tự.

2.Array#bsearch Mảng là một kiểu dữ liệu được sử dụng rất phổ biến trong lập trình, thông thường với số lượng phần tử nhiều chúng ta có thể sử dụng các hàm trong ruby như find, reject, select với module Enumerable, các hàm này sẽ xử lí lấy ra các phần tử lần lượt, giúp duyệt mảng nhanh hơn và không bị tràn. Tuy nhiên với khối lượng dữ liệu lưu vào mảng rất lớn thì các phương pháp trên vẫn chưa được tối ưu. Gỉa sử ta có 1 mảng có 50.000.000 phần tử, ta cần tìm ra các phần tử có gía trị lớn hơn 40.000.000, nếu sử dụng hàm select của enumerable, chúng ta cần duyệt tuần tự qua 50.000.000 phần tử đó, độ phức tạp của thuật toán lúc này là O(n). Với việc sử dụng hàm bsearch của ruby, độ phức tạp của thuật toán trên chỉ còn O(log n). Đây là kết qủa khi chạy thử benchmark trên phiên bản ruby 1.9



require 'benchmark'

data = (0..50_000_000)

Benchmark.bm do |x|
  x.report(:find) { data.find {|number| number > 40_000_000 } }
  x.report(:bsearch) { data.bsearch {|number| number > 40_000_000 } }
end

         user       system     total       real
find     3.020000   0.010000   3.030000   (3.028417)
bsearch  0.000000   0.000000   0.000000   (0.000006)

Như các bạn có thể thấy, bsearch nhanh hơn rất nhiều, tuy nhiên 1 điểm rất quan trọng cần lưu ý đó là bsearch đó là mảng cần phải được sắp xếp trước đó. Các bạn có thể cân nhắc sử dụng trong 1 số trường hợp như mảng id, created_at... được load ra từ database.

3. Enumerable#flat_map Khi làm việc với dữ liệu quan hệ, đôi khi chúng ta cần lấy ra một mảng các thuộc tính của một đối tượng nào đó. Gỉa sử bạn có 1 blog và bạn đang muốn lấy ra tên của tất cả những người đã comment vào các bài viết của 1 vài người dùng nào đó, code của bạn lúc đó trông sẽ như sau.



module CommentFinder
  def self.find_for_users(user_ids)
    users = User.where(id: user_ids)
    users.map do |user|
      user.posts.map do |post|
        post.comments.map |comment|
          comment.author.username
        end
      end
    end
  end
end

Và đây là kết qủa trả về:

[[['Ben', 'Sam', 'David'], ['Keith']], [[], [nil]], [['Chris'], []]]

Rõ ràng là các phần tử trong mảng lồng nhau qúa nhiều mà không theo từng bài viết, thông thường chúng ta sử dụng flattern ở các lần map để duỗi các mảng như sau:



module CommentFinder
  def self.find_for_users(user_ids)
    users = User.where(id: user_ids)
    users.map { |user|
      user.posts.map { |post|
        post.comments.map { |comment|
          comment.author.username
        }.flatten
      }.flatten
    }.flatten
  end
end

Tuy nhiên nếu sử dụng flat_map, bạn sẽ không cần sử dụng flattern nữa, nó giúp code chúng ta trông gọn gàng và chuyên nghiệp hơn:



module CommentFinder
  def self.find_for_users(user_ids)
    users = User.where(id: user_ids)
    users.flat_map { |user|
      user.posts.flat_map { |post|
        post.comments.flat_map { |comment|
          comment.author.username
        }
      }
    }
  end
end

Kết qủa thu được tương tự như khi dùng flattern nhiều lần 😄.

4. Array.new với tham số là block Thông thường khi bạn muốn khai báo 1 mảng trong ruby với 10 phần tử, bạn có thể sử dụng hàm Array.new(10), hàm này trả về 1 mảng 10 phần tử với gía trị rỗng. Vậy nếu muốn có mảng 10 phần tử với gía trị là 0 thì sao?. Ruby cho phép truyền vào hàm new của class Array 1 block, giá trị bên trong đó là giá trị mặc định của các phần tử, chúng ta có thể viết như sau:

Array.new(8) { 'O' }
#=> ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O']

Với mảng 2 chiều bạn cũng có thể làm tương tự. Giả sử bạn có đang làm game bắn tàu, mỗi bàn chơi là một mảng 2 chiều 8x8, với các gía trị mặc định là 0. Cách nhanh nhất để bạn tạo ra 1 bàn cờ như thế là gì?

class Board
  def board
    @board ||= Array.new(8) { Array.new(8) { '0' } }
  end
end

Với cách viết như trên chúng ta có thể tạo ra mảng n chiều với các giá trị mặc định bên trong.

5.Toán tử <=>

Toán tử <=> còn gọi là "spaceship" hoặc sort operator, nó là 1 toán tử được dựng sẵn trong hầu khắp các class của Ruby, nó khá hữu dụng khi sử dụng kèm với các hàm của module Enumerable. Để hiểu toán tử này hoạt động như thế nào, chúng ta cùng thử nó trên Class Fixnum

5<=>5
#=> 0
5<=>4
#=> 1
4<=>5
#=> -1

Vậy với số tự nhiên, nếu bằng nhau toán tử trả về 0, nếu lớn hơn trả về 1 và nhỏ hơn trả về -1.

Ứng dụng toán tử này với một bài toán thực tế như sau, chúng ta truyền vào 1 hàm số giờ và số phút, trả về số giờ và phút quy đổi ra, ví dụ truyền vào 1 giờ và 80 phút, chúng ta đổi ra 2h 20 phút.

  def fix_minutes hours, minutes
    until (0...60).member? minutes
       hours -= 60 <=> minutes
       minutes += 60 * (60 <=> minutes)
    end
    hours %= 24
    puts hours
    puts minutes
  end

Với mỗi số phút truyền vào, nếu lớn hơn 60 thì chúng ta so sánh với 60 để tăng số giờ lên 1 và trừ số phút đi 60, kết qủa cuối cùng trả về chính là số ngày, giờ và phút được quy đổi từ số phút ban đầu truyền vào. Một ví dụ khác, ta có 1 mảng các hash như sau:

a = [{:foo=>"foo", :bar=>2},
 {:foo=>"foo", :bar=>3},
 {:foo=>"foo", :bar=>5}]

Nếu muốn sắp xếp các hash bên trong mảng theo thứ tự value tăng dần, sử dụng toán tử spaceship ta có thể viết:

a.sort{|a,b| b[:bar] <=> a[:bar]}

Kết qủa trả về là

pry(main)> a.sort{|a,b| b[:bar] <=> a[:bar]}
=> [{:foo=>"foo", :bar=>5},
 {:foo=>"foo", :bar=>3},
 {:foo=>"foo", :bar=>2}]

Toán tử spaceship khá hữu ích với các thuật toán sắp xếp và xử lí số học, chỉ cần bạn nhớ bộ 3 số trả ra của nó bao gồm 0, 1, -1 , sẽ có nhiều khi cần đến đấy 😄. Trên đây là một số hàm khá hay mình tuyển tập lại trong qúa trình làm, nếu có ý kiến đóng góp các bạn có thể để lại bình luận bên dưới nhé. Cảm ơn các bạn đã theo dõi bài viết.

Tài liệu tham khảo


All Rights Reserved