シーザー暗号をRubyのメタプロとカリー化で実装してみる
シーザー暗号をRubyで実装する。
"Hello World!".caesar => "Khoor Zruog!"
となるような、String#caesarを定義する。RSpecを書くと、
require './caesar.rb' describe String do before(:all) do String.define_ceasar(3) end describe "caesar" do it "return Khoor Zruog! for Hello World!" do "Hello World!".caesar.should == "Khoor Zruog!" end end end
という感じ。
まず、Stringクラスのクラスメソッドとして、define_ceasar(n)を定義する。暗号のカギ(何文字分ずらすか)を渡すと、このメソッドは、Stringクラスのインスタンスメソッドとして、String#caesarを定義する。
class String class << self def define_ceasar(n) lower = ("a".."z").to_a.rotate(n).join higher = lower.upcase self.module_eval do define_method :caesar do str = self.tr('a-z', lower ) str = str.tr('A-Z', higher) end end end end end
ネストが深くなっている割に、self.module_evalのselfがStringというのが、ちょっと気持ち悪い。
いくらRubyがオープンクラスだからといって、Stringにいきなりceasarメソッドを定義するのってどうよ、というわけで、Stringクラスを継承したCryptMessageなんていうのを作ってみる。さらに、moduleによるmixinを使ってクラスメソッドを定義するクラス拡張というのをやってみる。
module Caesar def self.included(base) base.extend(ClassMethods) end module ClassMethods def define_caesar_method(n) lower = ("a".."z").to_a.rotate(n).join higher = lower.upcase self.module_eval do define_method :caesar do str = self.tr('a-z', lower ) str = str.tr('A-Z', higher) end end end end end class CryptMessage < String include Caesar end CryptMessage.define_caesar_method(3) puts CryptMessage.new("Hellow World!").caesar
Module#includedは、Ruby標準のフックで、mixinで読み込まれたときに、読み込み元のクラスを引数として与えられたブロックを実行する。この例ではbaseに渡されたmixin先のクラスをextendしていて、これは頻出のイディオムなので、Rails3からは、ActiveSupport::Concernをincludeすることで、もうちょっといい感じに書けるDSLが使える。
以下のように書いてみた。
require 'active_support' module Caesar extend ActiveSupport::Concern module ClassMethods def define_caesar_method(n) @l = ("a".."z").to_a.rotate(n).join @h = @l.upcase end end module InstanceMethods def caesar lower = self.class.module_eval { instance_variable_get(:l) } higher = self.class.module_eval { instance_variable_get(:h) } self.tr('a-z', lower).tr('A-Z', higher) end end end class CryptMessage < String include Caesar end CryptMessage.define_caesar_method(3) puts CryptMessage.new("Hello World!").caesar
どうも動いていない。こう、映画インセプションの夢の階層構造じゃないけど、ActiveSupport::Concernで抽象化した分、クラスインスタンス変数がどこに所属しているのかということが非常にわかりづらくなる、気がする。
さて、次に関数型っぽくクロージャで書いてみる。
def make_caesar(n) lower = ("a".."z").to_a.rotate(n).join higher = lower.upcase ->(str) { str.tr('a-z', lower).tr('A-Z', higher) } end c = make_caesar(3) puts c["Hello World"]
Proc#callは、実はProc#[]とも書ける。しかし、c["hoge"]ではハッシュのようにも見えるし、これはどうも良くない。というか、こういう書き方はRubyじゃない。関数型っぽく書けるといっても、ネイティブの関数型の文法が備わっているわけではなく、そんな書き方をする人もRubyコミュニティには少ないだろうし、こういうのは異物感が強い。
次に、もっと異物感の強いカリー化を使って書いてみる。
curried_caesar = ->(n, str) { lower = ("a".."z").to_a.rotate(n).join higher = lower.upcase str.tr('a-z', lower).tr('A-Z', higher) }.curry caesar3 = curried_caesar[3] puts caesar3["Hello World!"] caesar5 = curried_caesar[5] puts caesar5["Hello World!"]
わざわざ、Proc#curryと書かないといけないというので、異物感はさらに強まる。
と、一回りしたところで、Rubyで書く正解は、
module MyUtils module Caesar def self.encrypt(str, options) lower = ("a".."z").to_a.rotate(options[:key]).join upper = lower.upcase str.tr('a-z', lower).tr('A-Z', upper) end end end puts MyUtils::Caesar.encrypt("Hellow World!", key:3)
なのだろうなと思った。