【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)}
うーんすごい。