+1

Những tính năng mới trong Ruby 2.4

Thực thi Regular Expression nhanh hơn với Regexp#match?

Ruby 2.4 thêm một phương thức mới là #match? cho regular expression. Phương thức này nhanh gấp 3 lần so với tất cả các phương thức khác trong class Regexp ở Ruby 2.3:

require 'benchmark/ips'

Benchmark.ips do |bench|
  EMPTY_STRING  = ''
  WHITESPACE    = "   \n\t\n   "
  CONTAINS_TEXT = '   hi       '

  PATTERN = /\A[[:space:]]*\z/

  bench.report('Regexp#match?') do
    PATTERN.match?(EMPTY_STRING)
    PATTERN.match?(WHITESPACE)
    PATTERN.match?(CONTAINS_TEXT)
  end

  bench.report('Regexp#match') do
    PATTERN.match(EMPTY_STRING)
    PATTERN.match(WHITESPACE)
    PATTERN.match(CONTAINS_TEXT)
  end

  bench.report('Regexp#=~') do
    PATTERN =~ EMPTY_STRING
    PATTERN =~ WHITESPACE
    PATTERN =~ CONTAINS_TEXT
  end

  bench.report('Regexp#===') do
    PATTERN === EMPTY_STRING
    PATTERN === WHITESPACE
    PATTERN === CONTAINS_TEXT
  end

  bench.compare!
end

# >> Warming up --------------------------------------
# >>        Regexp#match?   160.255k i/100ms
# >>         Regexp#match    44.904k i/100ms
# >>            Regexp#=~    71.184k i/100ms
# >>           Regexp#===    71.839k i/100ms
# >> Calculating -------------------------------------
# >>        Regexp#match?      2.630M (± 4.0%) i/s -     13.141M in   5.004929s
# >>         Regexp#match    539.361k (± 3.9%) i/s -      2.694M in   5.002868s
# >>            Regexp#=~    859.713k (± 4.2%) i/s -      4.342M in   5.060080s
# >>           Regexp#===    872.217k (± 3.5%) i/s -      4.382M in   5.030612s
# >>
# >> Comparison:
# >>        Regexp#match?:  2630002.5 i/s
# >>           Regexp#===:   872217.5 i/s - 3.02x slower
# >>            Regexp#=~:   859713.0 i/s - 3.06x slower
# >>         Regexp#match:   539361.3 i/s - 4.88x slower

Khi các bạn gọi các method như Regexp#===, Regexp#=~ hoặc Regexp#match, Ruby sẽ gán kết quả MatchData vào biến toàn cục $~:

/^foo (\w+)$/ =~ 'foo bar'      # => 0
$~                              # => #<MatchData "foo bar" 1:"bar">

/^foo (\w+)$/.match('foo baz')  # => #<MatchData "foo baz" 1:"baz">
$~                              # => #<MatchData "foo baz" 1:"baz">

/^foo (\w+)$/ === 'foo qux'     # => true
$~                              # => #<MatchData "foo qux" 1:"qux">

Regexp#match? chỉ trả về kết quả là boolean và bỏ qua việc tạo đối tượng MatchData hoặc update lại biến toàn cục:

/^foo (\w+)$/.match?('foo wow') # => true
$~                              # => nil

Phương thức sum mới cho module Enumerable

Ở phiên bản mới này, bạn có thể gọi phương thức #sum cho bất kỳ đối tượng Enumerable nào:

[1, 2, 3, 4, 5].sum # => 15

Phương thức #sum có thể nhận thêm một tham số tùy chọn và nó có giá trị default là 0. Tham số này là khởi đầu của việc tính tổng, cho nên [].sum sẽ trả về 0.

Nếu các bạn gọi hàm #sum trên một mảng các đối tượng không phải là số (Integer) thì bạn phải tự thiết lập giá trị khởi đầu này:

class ShoppingList
  attr_reader :items

  def initialize(*items)
    @items = items
  end

  def +(other)
    ShoppingList.new(*items, *other.items)
  end
end

eggs   = ShoppingList.new('eggs')          # => #<ShoppingList:0x007f952282e7b8 @items=["eggs"]>
milk   = ShoppingList.new('milks')         # => #<ShoppingList:0x007f952282ce68 @items=["milks"]>
cheese = ShoppingList.new('cheese')        # => #<ShoppingList:0x007f95228271e8 @items=["cheese"]>

eggs + milk + cheese                       # => #<ShoppingList:0x007f95228261d0 @items=["eggs", "milks", "cheese"]>
[eggs, milk, cheese].sum                   # => #<TypeError: ShoppingList can't be coerced into Integer>
[eggs, milk, cheese].sum(ShoppingList.new) # => #<ShoppingList:0x007f9522824cb8 @items=["eggs", "milks", "cheese"]>

Như bạn đã thấy, nếu không tự thiết lập giá trị khởi đầu đúng với đối tượng trong Enumerable thì phương thức #sum sẽ hiểu là 0 + ShoppingList instance và từ đó sinh ra lỗi TypeError như trên.

Phương thức mới để kiểm tra thư mục hoặc file rỗng

Trong Ruby 2.4, bạn có thể kiểm tra xem thư mục hoặc file có rỗng hay không bằng cách sử dụng các module FileDir:

Dir.empty?('empty_directory')      # => true
Dir.empty?('directory_with_files') # => false

File.empty?('contains_text.txt')   # => false
File.empty?('empty.txt')           # => true

Phương thức File.empty? tương đương với phương thức File.zero? đã có sẵn trong các phiên bản Ruby trước:

File.zero?('contains_text.txt')  # => false
File.zero?('empty.txt')          # => true

Lấy các named capture từ kết quả trả về của Regexp

Trong Ruby 2.4, bạn có thể gọi phương thức #named_captures trên đối tượng kết quả trả về của một Regexp và nó sẽ cho bạn một hash chứa named captures và dữ liệu matching của nó:

pattern  = /(?<first_name>John) (?<last_name>\w+)/
pattern.match('John Backus').named_captures
# => { "first_name" => "John", "last_name" => "Backus" }

Ruby 2.4 còn thêm phương thức #values_at để lấy ra named capture mà bạn quan tâm:

pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
pattern.match('2016-02-01').values_at(:year, :month) # => ["2016", "02"]

Phương thức Integer#digits

Nếu bạn muốn truy cập vào một vị trí nào đó trong một số (Integer) thì bạn có thể gọi phương thức Integer#digits:

123.digits                  # => [3, 2, 1]
123.digits[0]               # => 3

# Equivalent behavior in Ruby 2.3:
123.to_s.chars.map(&:to_i).reverse # => [3, 2, 1]

Cải tiến interface của Logger

Trong Ruby 2.3, việc thiết lập Logger khá là phức tạp:

logger1 = Logger.new(STDOUT)
logger1.level    = :info
logger1.progname = 'LOG1'

logger1.debug('This is ignored')
logger1.info('This is logged')

# >> I, [2016-07-17T23:45:30.571508 #19837]  INFO -- LOG1: This is logged

Với Ruby 2.4, hàm khởi tạo của Logger nhận các options này:

logger2 = Logger.new(STDOUT, level: :info, progname: 'LOG2')

logger2.debug('This is ignored')
logger2.info('This is logged')

# >> I, [2016-07-17T23:45:30.571556 #19837]  INFO -- LOG2: This is logged

Phân tích (parse) các CLI option thành Hash

Việc phân tích các cờ của command line với OptionParser cần rất nhiều đoạn code để có thể đưa nó vào một hash:

require 'optparse'
require 'optparse/date'
require 'optparse/uri'

config = {}

cli =
  OptionParser.new do |options|
    options.define('--from=DATE', Date) do |from|
      config[:from] = from
    end

    options.define('--url=ENDPOINT', URI) do |url|
      config[:url] = url
    end

    options.define('--names=LIST', Array) do |names|
      config[:names] = names
    end
  end

Với Ruby 2.4, bạn có thể cung cấp một tham số keyword là :into khi phân tích các cờ của command line:

require 'optparse'
require 'optparse/date'
require 'optparse/uri'

cli =
  OptionParser.new do |options|
    options.define '--from=DATE',    Date
    options.define '--url=ENDPOINT', URI
    options.define '--names=LIST',   Array
  end

config = {}

args = %w[
  --from  2016-02-03
  --url   https://blog.blockscore.com/
  --names John,Daniel,Delmer
]

cli.parse(args, into: config)

config.keys    # => [:from, :url, :names]

Cải tiến về tốc độ đối với phương thức max, min của Array

Array trong Ruby 2.4 được định nghĩa các phương thức #min#max riêng của mình. Điều này làm cho tốc độ của 2 phương thức này trên các Array được cải thiện đáng kể:

     Array#min:       35.1 i/s
Enumerable#min:       21.8 i/s - 1.61x slower

Đơn giản hóa các lớp Integer

Với các phiên bản Ruby trước đây, các bạn sẽ phải làm việc với các kiểu số khác nhau:

# Find classes which subclass the base "Numeric" class:
numerics = ObjectSpace.each_object(Module).select { |mod| mod < Numeric }

# In Ruby 2.3:
numerics # => [Complex, Rational, Bignum, Float, Fixnum, Integer, BigDecimal]

# In Ruby 2.4:
numerics # => [Complex, Rational, Float, Integer, BigDecimal]

Ở Ruby 2.4, FixnumBignum sẽ được Ruby xử lý thay bạn. Nó sẽ giúp bạn tránh được một số bug như sau:

def categorize_number(num)
  case num
  when Fixnum then 'fixed number!'
  when Float  then 'floating point!'
  end
end

# In Ruby 2.3:
categorize_number(2)        # => "fixed number!"
categorize_number(2.0)      # => "floating point!"
categorize_number(2 ** 500) # => nil

# In Ruby 2.4:
categorize_number(2)        # => "fixed number!"
categorize_number(2.0)      # => "floating point!"
categorize_number(2 ** 500) # => "fixed number!"

Nếu các bạn vẫn sử dụng các constant BignumFixnum trong source code của mình thì cũng không vấn đề gì. Các constant này đều có giá trị là Integer:

Fixnum  # => Integer
Bignum  # => Integer

Tham số mới hỗ trợ các phương thức float

Trong ruby 2.4, #round, #ceil, #floor, và #truncate đều nhận thêm một tham số làm tròn

4.55.ceil(1)     # => 4.6
4.55.floor(1)    # => 4.5
4.55.truncate(1) # => 4.5
4.55.round(1)    # => 4.6

Những phương thức này cũng hoạt động trên cả Integer.

Case sensitivity với các kí tự Unicode

Hãy xem xét đến string sau:

My name is JOHN. That is spelled J-Ο-H-N

Khi gọi #downcase ở Ruby 2.3, kết quả của nó sẽ là:

my name is john. that is spelled J-Ο-H-N

Lý do là bởi "J-Ο-H-N" trong string trên được viết bởi các ký tự Unicode

Với Ruby 2.4 thì việc này đã được xử lý:

sentence =  "\uff2a-\u039f-\uff28-\uff2e"
sentence                              # => "J-Ο-H-N"
sentence.downcase                     # => "j-ο-h-n"
sentence.downcase.capitalize          # => "J-ο-h-n"
sentence.downcase.capitalize.swapcase # => "j-Ο-H-N"

Tùy chọn mới để thiết lập độ dài của một string

Khi tạo một string, bạn có thể định nghĩa một option :capacity. Nó sẽ cho Ruby biết là bao nhiêu bộ nhớ sẽ được sử dụng cho string của bạn. Điều này sẽ làm tăng performance của bạn khi mà nó giúp Ruby tránh việc khởi tạo bộ nhớ liên tục khi mà bạn tăng kích thước của string:

require 'benchmark/ips'

Benchmark.ips do |bench|
  bench.report("Without capacity") do
    append_me = ' ' * 1_000
    template  = String.new

    100.times { template << append_me }
  end

  bench.report("With capacity") do
    append_me = ' ' * 1_000
    template  = String.new(capacity: 100_000)

    100.times { template << append_me }
  end

  bench.compare!
end

# >> Warming up --------------------------------------
# >>     Without capacity     1.690k i/100ms
# >>        With capacity     3.204k i/100ms
# >> Calculating -------------------------------------
# >>     Without capacity     16.031k (± 7.4%) i/s -    160.550k in  10.070740s
# >>        With capacity     37.225k (±18.0%) i/s -    362.052k in  10.005530s
# >>
# >> Comparison:
# >>        With capacity:    37225.1 i/s
# >>     Without capacity:    16031.3 i/s - 2.32x slower

Sửa lại chức năng matching đối với Symbol

Trong Ruby 2.3, Symbol#match trả về vị trí match của Regexp khi mà String#match lại trả về đối tượng MatchData. Ruby 2.4 đã sửa lại điều này:

# Ruby 2.3 behavior:

'foo bar'.match(/^foo (\w+)$/)  # => #<MatchData "foo bar" 1:"bar">
:'foo bar'.match(/^foo (\w+)$/) # => 0

# Ruby 2.4 behavior:

'foo bar'.match(/^foo (\w+)$/)  # => #<MatchData "foo bar" 1:"bar">
:'foo bar'.match(/^foo (\w+)$/) # => #<MatchData "foo bar" 1:"bar">

Cải tiến việc thông báo các Exception cho threading

Nếu bạn gặp phải exception trong một thread thì thông thường, Ruby sẽ che đi exception này:

# parallel-work.rb
puts 'Starting some parallel work'

thread =
  Thread.new do
    sleep 1

    fail 'something very bad happened!'
  end

sleep 2

puts 'Done!'
$ ruby parallel-work.rb
Starting some parallel work
Done!

Nếu bạn muốn raise exception cho toàn bộ process khi có exception xảy ra trong một thread, thì bạn có thể dùng Thread.abort_on_exception = true. Nếu thêm dòng này vào đoạn code trên thì output sẽ như sau:

$ ruby parallel-work.rb
Starting some parallel work
parallel-work.rb:9:in 'block in <main>': something very bad happened! (RuntimeError)

Trong Ruby 2.4, bạn có thể đưa ra thông báo lỗi mà không cần phải dừng hẳn chương trình bằng cách sử dụng Thread.report_on_exception = true. Output:

$ ruby parallel-work.rb
Starting some parallel work
#<Thread:0x007ffa628a62b8@parallel-work.rb:6 run> terminated with exception:
parallel-work.rb:9:in 'block in <main>': something very bad happened! (RuntimeError)
Done!

Bài viết được dịch từ New feature in Ruby 2.4


All Rights Reserved

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