読者です 読者をやめる 読者になる 読者になる

せかいや

いまいるここを、おもしろく http://sekai-in-the-box.appspot.com/

【Ruby】case、ラムダ構文、高階関数、外部イテレーター、===演算子、zipメソッド、

 
■topic summary
study about "case-when" about Ruby.



例によって、hp12cさんの記事を元にお勉強。
Rubyのcaseを〇〇(言語名)のswitch文だと思っている人たちにぼくから一言ガツンと申し上げたい

hp12cさんの書くコードは「Rubyぽさ」がすごくよく表現されていて、勉強になる。。
この記事もちょっと長いけどいろんな発見があった!

他にはFiberの使い方とか
行列を使わないダイクストラ法の実装方法などを勉強させてもらってます。

 

問題

名前name、レベルlevel、ポイントpointの各属性を持った複数のCharacterオブジェクトcharlie, liz, benがある。
キャラクタのレベルに応じてポイントを加算するbonus_pointメソッドを実装せよ。但し、キャラクタレベルがlow(1〜3)のときは10ポイント、mid(4〜7)のときは5ポイント、high(8または9)のときは3ポイント、それ以外のときは0ポイントをpointに加算するものとする。

ふむふむ。。。
記事のテーマがcaseだったので、caseを使って書いてみた。

class Character < Struct.new(:name, :level, :point)
  def bonus_point
    bonus = 0
    case self.level
    when 1..3
      bonus = 10
    when 4..7
      bonus = 5
    when 8..9
      bonus = 3
    end
    self.point += bonus
  end
end
cha = Character.new('Cha', 5, 0)
liz = Character.new('Liz', 3, 0)
ben = Character.new('Ben', 8, 0)
charas = [cha, liz, ben]
charas.each{|x| x.bonus_point}
puts charas

when節ではRangeやクラスを記載することができる。
ここまでは知ってた。

すごいのはここから。

 

caseは式(値を返す)

こうやって帰り値を代入できる

def bonus_point
  bonus = case self.level
  when 1..3 then 10
  when 4..7 then 5
  when 8..9 then 3
  end
  self.point += bonus
end


 

when節にProcオブジェクトを取れる

 -> はラムダ構文。

def bonus_point
  is_low = ->lev{(1..3).include?(lev)}
  is_mid = ->lev{(4..7).include?(lev)}
  is_high = ->lev{(8..9).include?(lev)}
  bonus = case self.level
  when is_low then 10
  when is_mid then 5
  when is_high then 3
  end
  self.point += bonus
end

 

高階関数

関数を返す関数を実装する

class Character < Struct.new(:name, :level, :point)
  def bonus_point
    bonus = case self.level
    when low? then 10
    when mid? then 5
    when high? then 3
    end
    self.point += bonus
  end
end
private
def level_seed(range)
  ->lev{range.include?(lev)}
end
def low?
  level_seed(1..3)
end
def mid?
  level_seed(4..7)
end
def high?
  level_seed(8..9)
end

 
 

すごいね。

 

問題

仮想WebフレームワークRackのレスポンスは、3要素の配列、すなわちステータスコード(Fixnum), ヘッダ(Hash), レスポンスボディ(#eachに応答する)を要素とする配列で構成される。以下のレスポンスのうち、res1は有効、res2、res3は無効なレスポンスである

ふむふむ。。やってみよう。
caseを使って実装してみよう。

def response_lint(res)
  case res[0]
  when Fixnum
    #NOP
  else
    raise "#{res[0]}: error 1"
  end
  case res[1]
  when Hash
    #NOP
  else
    raise "#{res[0]}:error 2"
  end
  case res[2].class.method_defined?(:each)
  when true
    #NOP
  else
    raise "#{res[0]}:error 3"
  end
end
res1 = [200, {'Content-Type' => 'text/html'}, "Welcome to Rack".chars]
res2 = ['404', {'Content-Type' => 'text/html'}, "Welcome to Rack".chars]
res3 = [500, {'Content-Type' => 'text/html'}, "Welcome to Rack"]
[res1, res2, res3].each{|x| response_lint(x)}

 
こうかな?

以下はhp12cさんの記事を元にリファクタリング
  

class.method_defined? よりもrespond_to?

わざわざクラスオブジェクトを作らなくてよく、シンプル。
あと、例外発生ではなくBooleanを返すよう設計を修正

def response_lint(res)
  case res[0]
  when Fixnum
    #NOP
  else
    return false
  end
  case res[1]
  when Hash
    #NOP
  else
    return false
  end
  case res[2].respond_to?(:each)
  when true
    #NOP
  else
    return false
  end
  true
end

 
 

外部イテレーターを利用

外部イテレーターについては以前勉強した。

def response_lint(res)
  pattern = [Fixnum, Hash, Enumerable].to_enum
  res.each do |target|
    if target.is_a?(pattern.next)
    else
      return false
    end
  end
  true
end

 
 

===演算子を利用

ポイント: A===B not equal B===A です。

def response_lint(res)
  pattern = [Fixnum, Hash, Enumerable].to_enum
  res.each do |target|
    if pattern.next === target
    else
      return false
    end
  end
  true
end

 
 

zipメソッドを使う

やばいでしょこれは!
zip => all? はスマートすぎるわ。。

def response_lint(res)
  pattern = [Fixnum, Hash, Enumerable]
  pattern.zip(res).all?{|clazz, val| clazz === val}
end

zipメソッドは、冷凍のzipではなくて、
ファスナーをイメージするといいと思う。
別々の集合を互い違いに掛けあわせていくイメージ。
 
 

===演算子の実装

独自にクラスを作り、そこで===演算子を定義することで、
when節に そのクラスのインスタンスを置いてスマートに実装できる

class Pattern < Array
  def ===(res)
    self.zip(res).all?{|clazz, val| clazz === val}
  end
end
def response_lint(res)
  case res
  when Pattern[Fixnum, Hash, Enumerable]
  else
    return false
  end
  true
end

ポイントは、===演算子インスタンスメソッドとして定義しているところ。



さらにすごいことに。。。
 

独自クラスを要素として持つ独自クラスを作成

ポイントは===をクラスメソッドとして定義しているところ。
any?メソッド、はじめて使った。

class Pattern < Array
  def ===(res)
    self.zip(res).all?{|clazz, val|clazz === val}
  end
end
class HTTP_STATUS
  CODES = Rack::Utils::HTTP_STATUS_CODES
  def self.===(num)
    CODES.any?{|k,_|k == num}
  end
end
def response_lint(res)
  case res
  when Pattern[HTTP_STATUS, Hash, Enumerable]
  else
    return false
  end
  true
end

 
自分が驚いたのはこの次!

 

class HTTP_STATUS
  CODES = Rack::Utils::HTTP_STATUS_CODES
  def self.===(num)
    CODES.any?{|k,_|k == num}
  end
end

HTTP_STATUS = Rack::Utils::HTTP_STATUS_CODES
def HTTP_STATUS.===(num)
  self.any?{|k,_|k == num}
end

って書いてる。

かっこよすぎる!
独自クラスをわざわざつくらなくても、
オブジェクトに特異メソッドを持たせればいい、という発想。
確かに。

 

定数を動的に作成

定数を動的に作成して、そこに特異メソッド(===)を持つオブジェクトを代入する。

コード全文。

require "rack"
class Pattern < Array
  def ===(res)
    self.zip(res).all?{|clazz, val|clazz === val}
  end
end
HTTP_STATUS = Rack::Utils::HTTP_STATUS_CODES.group_by{|k,_|k/100}
(1..HTTP_STATUS.size).each do |i|
  status = HTTP_STATUS[i]
  def status.===(num)
    self.any?{|k,_|k == num}
  end
  Object.const_set("HTTP_STATUS_#{i}xx", status)
end
def response_lint(res)
  case res
  when Pattern[HTTP_STATUS_1xx, Hash, Enumerable] then "1xx"
  when Pattern[HTTP_STATUS_2xx, Hash, Enumerable] then "2xx"
  when Pattern[HTTP_STATUS_3xx, Hash, Enumerable] then "3xx"
  when Pattern[HTTP_STATUS_4xx, Hash, Enumerable] then "4xx"
  when Pattern[HTTP_STATUS_5xx, Hash, Enumerable] then "5xx"
  else false
  end
end
res1 = [200, {'Content-Type' => 'text/html'}, "Welcome to Rack".chars]
res2 = ['404', {'Content-Type' => 'text/html'}, "Welcome to Rack".chars]
res3 = [500, {'Content-Type' => 'text/html'}, "Welcome to Rack"]
res4 = [301, {'Content-Type' => 'text/html'}, "Welcome to Rack".chars]
res5 = [502, {'Content-Type' => 'text/html'}, "Welcome to Rack".chars]
res6 = [700, {'Content-Type' => 'text/html'}, "Welcome to Rack".chars]

p [res1, res2, res3, res4, res5, res6].map{|x| response_lint(x)}

 

うーんすごい。