12/08/2018, 16:41

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.

0