『pnmフォーマットファイルをCLOSで作ってみよう』のRubyへの翻訳

http://d.hatena.ne.jp/ytakenaka/20070620/p1
http://d.hatena.ne.jp/ytakenaka/20070624/p1

CLOSの理解のためRubyに翻訳してみました。

module RubyPNM                  # Rubyのモジュールは名前空間も管理
  module PNMDataInfo            # インスタンスを持たないクラスはモジュール
    attr_accessor :width, :height, :depth # 一発アクセサ宣言

    def init_data_info(width, height, depth=255)
      self.width = width
      self.height = height
      self.depth = depth
    end
  end

  module PNMHeader
    include PNMDataInfo         # モジュールの継承
    attr_accessor :id, :comment

    def comment=(sequence)      # set_hoge(obj, v)は obj.hoge=v と書くのがRuby Way
      @comment = "# " << sequence
    end

    def write_pnm(filename)
      open(filename, "w") do |stream|
        stream << "%s\n" % id
        stream << "%s %s\n" % [width, height]
        stream << "%s\n" % comment if comment
        stream << "%s\n" % depth
      end
    end
  end

  module PNMData
    include PNMDataInfo
    attr_accessor :data

    def init_data(width, height, depth=255)
      init_data_info(width, height, depth)
      self.data = Array.new(width) { Array.new(height) } # width x height の二次元配列
    end

    def set_data(x, y, value)
      case value
      when Array
        if value.length == 3
          # 真意がわかればビットシフト演算とビットOR演算に翻訳できるよね
          # (mapcar #'(lambda(v p)
          #         (setf (ldb (byte 8 p) (aref data x y)) v))
          #         value (list 0 8 16))
          data[x][y] = value[0] | value[1] << 8 | value[2] << 16
        else
          raise TypeError
        end
      when Fixnum
        data[x][y] = value
      else
        raise TypeError
      end

    end
  end

  class PPMBin                  # インスタンスを持つクラスはclassで
    include PNMHeader
    include PNMData

    def initialize() self.id = :P6 end

    def write_pnm(filename)
      # :beforeメソッドを呼ぶんじゃなくてsuperclassのメソッドを呼ぶ。
      # call-next-methodみたいなの。
      super                     

      open(filename, "a") do |stream|
        height.times do |y|     # dotimes
          width.times do |x|
            pix = data[x][y]
            # バイナリの読み書きにはperlでおなじみpack/unpack。
            stream.write [pix.rgb_red, pix.rgb_green, pix.rgb_blue].pack("C*")
          end
        end
      end
    end
  end

  class PPMText
    include PNMHeader
    include PNMData

    def initialize() self.id = :P3 end

    def write_pnm(filename)
      super
      open(filename, "a") do |stream|
        height.times do |y|
          width.times do |x|
            pix = data[x][y]
            stream.printf "%d %d %d " % [pix.rgb_red, pix.rgb_green, pix.rgb_blue]
          end
          stream.puts
        end
      end
    end
  end
end

# こともあろうにfixnumにメソッドを追加しちゃることに
class Fixnum
  def rgb_red
    self & 0xFF                 # (ldb (byte 8 0) ,pix)
  end

  def rgb_green
    (self >> 8) & 0xFF          # (ldb (byte 8 8) ,pix)
  end

  def rgb_blue
    self >> 16                  # (ldb (byte 8 16) ,pix)
  end
end

if __FILE__==$0
  include RubyPNM
  bin = PPMBin.new
  bin.init_data 100, 100
  bin.comment = "This is binary format."
  text = PPMText.new
  text.init_data 100, 100
  text.comment = "This is text format."
  100.times do |i|
    100.times do |j|
      args = [j, i, [2*i, 2*j, i+j]]
      bin.set_data *args
      text.set_data *args
    end
  end
  bin.write_pnm "ruby.bin.ppm"
  text.write_pnm "ruby.text.ppm"
end

んー、Common Lispだと1.44倍も冗長なんだなぁ…。

$ ls -l pnm.lisp pnm.rb 
-rw-r--r-- 1 rubikitch users 3676 2007-06-25 01:34 pnm.lisp
-rw-r--r-- 1 rubikitch users 2547 2007-06-25 01:48 pnm.rb