12/08/2018, 17:24

Extending the ruby case statement

Case statement là một trong những cấu trúc mạnh nhất của ngôn ngử lập trình, riêng Ruby case statement thường bị đánh giá thấp trong việc code hàng ngày. Ruby dùng phương thức so sánh bằng === để so sánh tương ứng trong case statement. Trong bài này sẽ tiếp cận về thêm tùy chọn hành vi cho case ...

Case statement là một trong những cấu trúc mạnh nhất của ngôn ngử lập trình, riêng Ruby case statement thường bị đánh giá thấp trong việc code hàng ngày.

Ruby dùng phương thức so sánh bằng === để so sánh tương ứng trong case statement. Trong bài này sẽ tiếp cận về thêm tùy chọn hành vi cho case statement.

Proc như một match target

Một thuận lợi của proc là phương thức case equality của nó cho kết quả thực hiện proc của chính nó, có ích và nhanh chóng cho matching conditions:

def palindrome? word
  case word
  when proc{|w| w.reverse == w }
    true
  else
    false
  end
end

Mã có hiệu quả hơn với custom matchers

Custom matchers có thể giúp chúng ta code hiệu qủa hơn và gần hơn với ý định.

DivisibilityMatcher = Struct.new(:divisor) do
  def ===(target)
    target % divisor == 0
  end
end

def divisible_by(num)
  DivisibilityMatcher.new(num)
end

case 18
when divisible_by(5)
  puts "divisible by 5"
when divisible_by(3)
  puts "divisible by 3"
when divisible_by(2)
  puts "divisible by 2"
end

=> divisible by 3

Matchers cho membership evaluation

Sử dụng một case đơn giản để mở rộng case xử lý membership evaluation:

MembershipEvaluationMatcher = Struct.new(:collection) do
  def ===(target)
    collection.include? target
  end
end

def member_of collection
  MembershipEvaluationMatcher.new collection
end

case 1
when member_of(%w[a b c])
  puts 'case 1'
when member_of([1,2,3])
  puts 'case 2'
end

=> case 2

Matcher composition

Lợi thế của việc có các matcher object chỉ xử lý việc kết hợp là chúng ta có thể tạo ra theo những cách nhìn từ quan điểm của sự kết hợp.

Hãy mở rộng kiểm tra chia để minh họa phần trên. Bạn có thể nhận thấy rằng cách viết trên không rơi vào các điều kiện - vì vậy trong khi chia hết cho 3, còn chia hết cho 2 thì không. Hãy kiểm tra cả hai bằng cách sử dụng một matcher.

CompositeDivisibilityMatcher = Struct.new(:matchers) do
  def ===(target) 
    combine_results matchers.map{|m| m === target }
  end

  def combine_results match_results
    false
  end
end

class AllDivisibilityMatcher < CompositeDivisibilityMatcher 
  def combine_results match_results
    match_results.reduce(:&)
  end
end

class AnyDivisibilityMatcher < CompositeDivisibilityMatcher
  def combine_results match_results
    match_results.reduce(:|)
  end
end

DivisibilityMatcher = Struct.new(:divisor) do
  def ===(target)
    target % divisor == 0
  end

  def &(matcher)
    AllDivisibilityMatcher.new([self, matcher])
  end

  def |(matcher)
    AnyDivisibilityMatcher.new([self, matcher])
  end

end

def divisible_by(num)
  DivisibilityMatcher.new(num)
end

(1..100).each do |num|
  case num
  when divisible_by(3) & divisible_by(5)
    puts 'FizzBuzz'
  when divisible_by(3)
    puts 'Fizz'
  when divisible_by(5)
    puts 'Buzz'
  else
    puts num
  end
end

Chúng ta có thể tận dụng proc currying giảm một số thừ bị thừa.

is_divisible = proc {|a,b| a % b == 0 }
is_divisible_by_all = proc {|arr, a| arr.map(&is_divisible.curry[a]).reduce(:&) }
is_divisible_by_any = proc {|arr, a| arr.map(&is_divisible.curry[a]).reduce(:|) }

case 18
when is_divisible_by_all.curry[[2, 3]]
puts 'div by 2 and 3'
when is_divisible_by_any.curry[[2, 3]]
puts 'div by 2 or 3'
end

=> div by 2 and 3

Matching against incomplete data structures

Chúng ta có thể mở rộng khái niệm này để phù hợp với cấu trúc dữ liệu tương tự chưa được xác định. Ở ví dụ dưới chúng ta kết hợp một list mà các giá trị bị thiếu được chú thích bằng :_. Case statement sẽ bỏ quả các phần tử khi matching.

module Matchers
  ListMatcher = Struct.new(:matchable_list) do
    def ===(list)
      return false unless list.length == matchable_list.length
      list.each_with_index do |item, idx|
        case matchable_list[idx]
        when :_, item
          next
        else
          return false
        end       
      end
      true
    end 
  end
end

def matches array
  Matchers::ListMatcher.new array
end

case [1,2,3]
when matches([1, :_ , 2, 3])
  puts "case 1"
when matches([4, 5, 6])
  puts "case 2"
when matches([:_, 1, 2])
  puts "case 3"
when matches([1, :_ , 3])
  puts "case 4"
end

# Outputs: case 4

Truy xuất các mục dữ liệu bị thiếu

Nếu chúng ta so sánh với cấu trúc dữ liệu không đầy đủ thì có thể tốt hơn khi truy xuất các phần tử thiếu đó. Việc này đòi hỏi thêm bản mẫu.

module Matchers

  Matchable = Struct.new(:target) do
    def matches
      @matches ||= []
    end
  end

  ListMatcher = Struct.new(:matchable_list) do
    def ===(list)
      return false unless list.target.length == matchable_list.length

      match_results = []

      list.target.each_with_index do |item, idx|
        case matchable_list[idx]
        when :_
          match_results << item
          next
        when item
          next
        else
          return false
        end       
      end

      list.matches << match_results
      true
    end 
  end

end

def matches array
  Matchers::ListMatcher.new array
end

matchable = Matchers::Matchable.new([1,2,3])

case matchable
when matches([1, :_ , 2, 3])
  puts "case 1"
when matches([4, 5, 6])
  puts "case 2"
when matches([:_, 1, 2])
  puts "case 3"
when matches([1, :_ , 3])
  puts "case 4"
end

puts matchable.matches
 => [[2]]

Khi chạy kết hợp các câu lệnh trường hợp nhều lần sẽ tiếp tục nối các kết quả khợp vào danh sách kết hợp.

Đến đây chúng ta đã xem qua một số các sử dụng của case mà nó có thể xây dựng thành cấu trúc để minh tận dụng lợi thế của nó.

Bài này được dịch ra từ bài gốc Extending the ruby case statement for fun and profit

0