シーザー暗号を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)

なのだろうなと思った。