5 hàm Ruby bạn nên sử dụng
Chúng ta đều biết Ruby sẽ dạy chúng ta cách thể hiện ý tưởng của mình vào một cái máy tính. Và đó là một trong những lý do chính làm cho Ruby trở thành một lựa chọn phổ biến cho phát triển web. Giống như các ngôn ngữ khác, có rất nhiều cách để thực hiện một vấn đề trong Ruby. Tuy nhiên, cách giải ...
Chúng ta đều biết Ruby sẽ dạy chúng ta cách thể hiện ý tưởng của mình vào một cái máy tính. Và đó là một trong những lý do chính làm cho Ruby trở thành một lựa chọn phổ biến cho phát triển web. Giống như các ngôn ngữ khác, có rất nhiều cách để thực hiện một vấn đề trong Ruby. Tuy nhiên, cách giải quyết đó có đơn giản hay không, phụ thuộc và method mà chúng ta chọn Trong bài viết này, tôi sẽ đề cập đến một vài method ít gặp để giải quyết các vấn đề cụ thể nào đó.
1. Object#tap
Khi gọi một method cho một vài object, nhưng giá trị trả về không phải là kết quả mong muốn, tôi muốn kết quả là object, nhưng lại là một vài giá trị khác.
VD, khi muốn thêm một giá trị bất kỳ vào một tập tham số được lưu trong hash, ta sử dụng Hash [], nhưng kết quả trả về lại là 'bar' thay vì param hash , vì vậy ta phải trả về trực tiếp
def update_params(params) params[:foo] = 'bar' params end
Dòng params không thực hiện xử lý gì ngoài việc return params, vì vậy, ta có thể xoá đi bằng method Object#tap. Chỉ cần truyền một block vào hàm tap với đoạn code mà bạn muốn thực hiện. Object sẽ được yield với block đó và sau đó sẽ trả về object đã được xử lý.
def update_params(params) params.tap {|p| p[:foo] = 'bar' } end
Bạn có thể sử dụng khi update object
User.first.update_attributes name: 'new name' => true User.first.tap{|user| user.update_attributes name: 'new name'} => #<User id: 1, name: 'new name', ...>
Với cách này, bạn có thể bớt được 1 dòng code ko đáng có và code sẽ dễ hiểu hơn
2. Array#bsearch
Tôi không biết bạn thế nào, nhưng tôi đã phải xử lý rất nhiều về mảng data. Ruby enumerables đã làm cho điều đấy trở nên dễ dàng hơn bằng cách cung cấp nhiều công cụ mà tôi sử dụng rất nhiều, đấy là select, reject và find. Nhưng với tập dữ liệu lớn, thì đó là một vấn đề không nhỏ.
Nếu bạn sử dụng ActiveRecord và SQL database, có rất nhiều magic đằng sau đó, làm cho các tìm kiếm của bạn trở nên đơn giản, ít thuật toán phức tạp. Nhưng thỉnh thoảng, bạn phải lấy toàn bộ data từ database trước khi xử lý chúng. VD, nếu các record được mã hoá trong database, bạn không thể truy vấn dễ dàng với SQL
Và khi vậy, tôi thấy thật khó khăn để làm thế nào sàng lọc dữ liệu với một thuật toán Bio-O có độ phức tạp nhất tôi có thể. Thuật toán có thể tốn nhiều hoặc ít thời gian, phụ thuộc vào độ phức tạp, được sắp xếp theo thứ tự: O(1), O(log n), O(n), O(n log(n)), O(n^2), O(2^n), O(n!). Chúng ta đều muốn là O(1).
Khi thực hiện tìm kiếm mảng trong Ruby, hàm đầu tiên được nghĩ đến là Enumerable#find, hoặc là detect. Tuy nhiên, hàm này sẽ tìm kiếm toàn bộ danh sách tới khi tìm thấy kết quả. Sẽ thật tốt nếu record ở ngay đầu, nhưng sẽ là vấn đề nếu nằm ở cuối một danh sách dài. Có thể đạt độ phức tạp O(n).
Thật may mắn, có một cách nhanh hơn. Array#bsearch có thể tìm thấy kết quả chỉ với độ phức tạp O(log n). VD, với Binary search Dưới đây là sự khác nhau trong thời gian tìm kiếm giữa 2 cách khi tìm kiếm trong dãy 50,000,000 số:
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ư bạn thấy, bsearch nhanh hơn nhiều. Tuy nhiên, có một vấn đề khá lớn liên quan đến việc sử dụng bsearch: Các mảng phải được sắp xếp. Mặc dù điều này phần nào giới hạn tính hữu dụng của nó, nhưng vẫn cần lưu ý đến những dịp mà nó có thể có ích - chẳng hạn như tìm một bản ghi bằng created_at đã tạo đã được tải từ cơ sở dữ liệu.
3. Enumerable#flat_map
Khi xử lý dữ liệu quan hệ, đôi khi chúng ta cần thu thập một tập các thuộc tính không liên quan và trả lại chúng trong một mảng không được lồng nhau. Hãy tưởng tượng bạn có một ứng dụng blog và bạn muốn tìm các tác giả của các nhận xét về bài viết được viết trong tháng trước bởi một nhóm người dùng nhất định.
VD:
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
Kết quả trả về sẽ là:
[[['Ben', 'Sam', 'David'], ['Keith']], [[], [nil]], [['Chris'], []]]
Và muốn làm đẹp hơn kết quả thì ta có thể sử dụng flatten
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
Nhưng có một cách khác gọn nhẹ hơn đó là sử dụng flat_map
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
Rất đơn giản , còn hơn là phải gọi flatten quá nhiều lần
4. Array.new với Block
Khi cần tạo một mảng 8x8 cho một class:
class Board def board @board ||= Array.new(8) { Array.new(8) { '0' } } end end
Và điều gì sẽ xảy ra khi chúng ta gọi Array.new với một tham số, nó sẽ tạo ra mảng với đúng kích thước đó
Array.new(8) #=> [nil, nil, nil, nil, nil, nil, nil, nil]
Và khi truyền vào một block, thì nó sẽ nhận mỗi phần tử của mảng là kết quả của block đó
Array.new(8) { 'O' } #=> ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O']
Vì vậy, nếu bạn truyền một mảng với tám phần tử một block để tạo ra một mảng với tám phần tử là 'O', bạn sẽ nhận được một mảng 8x8 được chứa các chuỗi 'O'.
Sử dụng mảng Array#new với một block, bạn có thể tạo ra tất cả các loại mảng đủ thể loại với dữ liệu mặc định và bất kỳ độ dài nào.
5. <=>
Toán tử spaceship - hoặc toán tử sort xuất hiện trong hầu hết các lớp được xây dựng trong Ruby, và rất hữu ích khi làm việc với các số enumerables.
Để minh hoạ nó hoạt động như thế nào, chúng ta hãy xem nó hoạt động như thế nào với Fixnums. Nếu bạn gọi 5 <=> 5, nó trả về 0. Nếu bạn gọi 4 <=> 5, nó sẽ trả về -1. Nếu bạn gọi 5 <=> 4, nó trả về 1. Về cơ bản, nếu hai số giống nhau, nó sẽ trả về 0, nếu không thì nó sẽ trả về -1 khi so sánh ít nhất đến lớn hơn và 1 khi ngược lại.
Tại sao ta nên sử dụng <=>
VD, có class Clock, ta muốn chỉnh giờ và phút bằng method + và -. Vấn đề đặt ra là sẽ khó khăn khi muốn thêm hơn 60ph, vì khi đó giá trị phút là không hợp lệ. Vì vậy, sẽ phải tăng giờ lên 1 và giảm số phút về 0.
def fix_minutes until (0...60).member? minutes @hours -= 60 <=> minutes @minutes += 60 * (60 <=> minutes) end @hours %= 24 self end
Nó hoạt động như thế này: cho đến khi phút chạy từ 0 đến 60, ta sẽ trừ đi hoặc là 1 hoặc -1 từ số giờ, tùy thuộc vào số phút đó lớn hơn 60. Sau đó điều chỉnh phút, thêm -60 hoặc 60 tùy thuộc vào thứ tự sắp xếp.