Hatena::Groupcoders

ラシウラ出張所 このページをアンテナに追加 RSSフィード

2007/06/10

[] RunHaskell moduleデモ  RunHaskell moduleのデモ - ラシウラ出張所 を含むブックマーク はてなブックマーク -  RunHaskell moduleのデモ - ラシウラ出張所  RunHaskell moduleのデモ - ラシウラ出張所 のブックマークコメント

で書いたような

  result = RunHaskell.run do
    main!(:mfoldr, "[]", [1,2,3]).where(%q{
      mfoldr = foldr (\elem bot -> "[" ++ (show elem) ++ ", " ++ bot ++ "]")
    })
  end

という感じで使えるもの。

とりあえず版だけど、show出力はパーズしてRubyオブジェクトにしてる。

require "tempfile"

module RunHaskell

  class HsRunner
    @@command = "runhaskell"
    def run(source)
      file = Tempfile.new "runhaskell"
      fname = file.path + ".hs"
      File.rename file.path, fname
      hsfile = File.new(fname, "w")
      hsfile.puts(source)
      hsfile.close
      io = IO.popen(@@command + " " + hsfile.path)
      result = io.read()
      io.close
      File.unlink hsfile.path
      result
    end
  end

  class HsParser
    def initialize(code)
      @code = code
      @index = 0
      @tokens = []
      tokenize
    end
    def parse()
      tok = shift
      case tok[0]
      when :numeric
        return HsNumeric.new(eval(tok[1]))
      when :string
        return HsString.new(eval(tok[1]))
      when :identifier
        return HsData.new(tok[1].to_sym, parse())
      when :lparen
        elems = []
        while true
          elem = parse()
          break if elem == nil
          elems << elem
          tok = shift
          case tok[0]
          when :rparen
            break
          when :comma
            next
          else
            unshift tok
            return nil
          end
        end
        return HsTuple.new(elems)
      when :lbracket
        elems = []
        while true
          elem = parse()
          break if elem == nil
          elems << elem
          tok = shift
          case tok[0]
          when :rbracket
            break
          when :comma
            next
          else
            unshift tok
            return nil
          end
        end
        return HsList.new(elems)
      when :lbrace
        elems = {}
        while true
          tok = shift
          unless tok[0] == :identifier
            unshift tok
            break
          end
          key = tok[1]
          tok = shift
          unless tok[0] == :equal
            unshift tok
            return nil
          end
          elem = parse()
          break if elem == nil
          elems[key] = elem
          tok = shift
          case tok[0]
          when :rbracket
            break
          when :comma
            next
          else
            unshift tok
            return nil
          end
        end
        return HsRecordPart.new(elems)
      else
        unshift tok
        return nil
      end
      nil
    end

    def tokenize
      while @index < @code.size
        token =  next_token(@code[(@index)..-1])
        @index += token[1].size
        @tokens << token unless token[0] == :space
      end
    end
    def shift()
      @tokens.shift
    end
    def unshift(token)
      @tokens.unshift token
    end

    def next_token(code)
      case code
      when /^(\s+)/
        [:space, $1]
      when /^(\{)/
        [:lbrace, $1]
      when /^(\})/
        [:rbrace, $1]
      when /^(\[)/
        [:lbracket, $1]
      when /^(\])/
        [:rbracket, $1]
      when /^(\()/
        [:lparen, $1]
      when /^(\))/
        [:rparen, $1]
      when /^([,])/
        [:comma, $1]
      when /^([=])/
        [:equal, $1]
      when /^([A-Za-z_][A-Za-z0-9_]*)/
        [:identifier, $1]
      when /^([-+]?[0-9]+([.][0-9]+)?(e[0-9]+)?)/
        [:numeric, $1]
      when /^(\"(\\\"|[^"])*\")/
        [:string, $1]
      end
    end
  end

  class HsNumeric
    def initialize(str)
      @core = str
    end
    def to_hs
      @core.to_s
    end
    def to_rb
      @core
    end
  end
  class HsString
    def initialize(str)
      @core = str
    end
    def to_hs
      core = @core.to_s.sub("\\", "\\\\")
      core = core.sub("\"", "\\\"")
      core = core.sub("\n", "\\n")
      core = core.sub("\r", "\\r")
      core = core.sub("\t", "\\t")
      core = core.sub("\b", "\\b")
      result = "\"" + core + "\""
      result
    end
    def to_rb
      @core
    end
  end
  class HsList
    def initialize(array)
      @core = array
    end
    def to_hs
      result = "["
      i = 0
      @core.each do |elem|
        i += 1
        result += elem.to_hs
        result += ", " unless i == @core.size
      end
      result += "]"
      result
    end
    def to_rb
      result = []
      @core.each do |elem|
        result << elem.to_rb
      end
      result
    end
  end
  class HsTuple
    def initialize(array)
      @core = array
    end
    def to_hs
      result = "("
      i = 0
      @core.each do |elem|
        i += 1
        result += elem.to_hs
        result += ", " unless i == @core.size
      end
      result += ")"
      result
    end
    def to_rb
      result = []
      @core.each do |elem|
        result << elem.to_rb
      end
      result
    end
  end
  class HsData
    def initialize(name, child)
      @name = name
      @child = child
    end
    def to_hs
      result = @name.to_s
      result += @child.to_hs unless @child
      result
    end
    def to_rb
      @child.to_rb unless @child
    end
  end
  class HsRecordPart
    def initialize(child)
      @child = child
    end
    def to_hs
      result = "{"
      i = 0
      @core.each do |key, elem|
        i += 1
        result += key.to_s + " = " + elem.to_hs
        result += ", " unless i == @core.size
      end
      result += "}"
      result
    end
    def to_rb
      result = {}
      @core.each do |key, elem|
        result[key] = elem.to_rb
      end
      result
    end
  end

  class Source
    def initialize(source="")
      @source = source
      @main = nil
    end
    def main!(name, *args)
      @main = Decl.new(:main)
      body = "putStrLn.show $ " + name.to_s
      args.each do |arg|
        body += " " + arg.to_hs
      end
      body += "\n"
      @main.body = body
      @main
    end
    def to_s
      source = @source
      source += "\n" + @main.to_s if @main
      source
    end
  end

  class Decl
    def initialize(name, *args)
      @name = name
      @args = args
      @body = ""
      @where = nil
    end
    attr_accessor :body
    def where(source)
      @where = source
    end
    def to_s
      result = @name.to_s
      @args.each do |arg|
        result += " " + arg.to_hs
      end
      result += " = " + @body
      if @where
        result += " where \n"
        result += @where
      end
      result
    end
  end

  def self.run(code="", &block)
    src = Source.new code
    src.instance_eval(&block) if block
    runner = HsRunner.new
    result = runner.run(src.to_s)
    parser = RunHaskell::HsParser.new(result)
    parser.parse.to_rb
  end
end

class String
  def to_hs
    RunHaskell::HsString.new(self).to_hs
  end
end
class Numeric
  def to_hs
    RunHaskell::HsNumeric.new(self).to_hs
  end
end
class Array
  def to_hs
    hs_list.to_hs
  end
  def hs_list
    RunHaskell::HsList.new(self)
  end
  def hs_tuple
    RunHaskell::HsTuple.new(self)
  end
end
class Hash
  def hs_data(name)
    RunHaskell::HsData.new(name, RunHaskell::HsRecordPart.new(self))
  end
end
class Symbol
  def to_hs
    self.to_s
  end
end

2007/04/18

[] RSpecことはじめ  RSpecことはじめ - ラシウラ出張所 を含むブックマーク はてなブックマーク -  RSpecことはじめ - ラシウラ出張所  RSpecことはじめ - ラシウラ出張所 のブックマークコメント

(記述を1.0.0対応しました)

RSpecRubyでBehaviour Driven Development(BDD)を行うための環境

BDDとは、Test Driven Developmentの発展形というか、より新機能開発に特化したもの。

Testは(assert_equalsのように)ふつう条件を満たすという視点で記述を行なうが、BDD仕様を宣言し、それはこうこうあるべき(should)という視点で記述する、という違いになります。

インストール

gemから

gem install rspec

rspecを入れると、binにspecというコマンドが追加されます。

RSpecことはじめ

Ruby組み込みクラスArrayを使ってspecを書いてみます。

ArraySpec.rb

describe "An Array" do
  before do
    @array = []
  end
  it "shoud be empty" do
    @array.should be_empty
  end
  it "shoud append at last when send #<<" do
    @array << "0"
    a = "a"
    @array << a
    @array.last.should equal(a)
  end
end

describeではArrayといった仕様対象のオブジェクトを書き、itでは仕様記述します。(0.8.2のころはdescribe/itcontext/specifyでしたが、deprecatedになりました)。

beforeで、コンテキスト対象のオブジェクトフィールド初期化するブロックをおきます(以前はsetupでしたが、beforeが基本になりました)。itごとに行うbefore(:each)describeごとに行うbefore(:all)があります。

仕様は二つ書きました。

  • 何もしないとき空
  • <<で送ったものは最後にある

最初の仕様の中身

    @array.should be_empty

shouldRSpecのコアクラス拡張で受け付けるようになったメソッドです。be_emptyはこのdoの実行オブジェクトが受け付けるメソッドで、shouldと連携しているようです。@array.empty?trueであることをチェックするものになります(block中のこのメソッドの使い方はとても面白い)。


以前は、

    @array.should_be_empty

と書けたのですが、0.9以降ではこの記法は受け付けなくなりました。

(Spec::Expectationsの)メソッドはshouldshould_notだけになりました。


二つ目の仕様

    @array.last.should equal(a)

    @array.last.should_equal a

でも0.8.2時代はいけたのですが、これらも同様に受け付けなくなりました。

実行したら以下のように失敗なく終わるでしょう

$ spec ArraySpec.rb

..

Finished in 0.002134 seconds

2 specifications, 0 failures

should ~のありか

shouldで受け付けるものの解説ドキュメント

ですが、ここにはshouldshould_notだけになりました。

~の仕様Spec::Matchersにあるものになります:

複数のコンテキストは分割する

先ほどのspecでは、ひとつのdescribeの中にbeforeで空のインスタンス作成、emptyの仕様記述、appendの仕様記述が入っていました。

もし、中の入ったArrayインスタンスの場合どうなるでしょうか。emptyの仕様は別のもの。appendの仕様記述は同じものになります。

このような状況で仕様を書くには、インスタンス化して入力条件を作る部分と、アクションと事後条件チェックする部分を分割し、組み合わせるのが理知的です。RSpecでは以下のようにdescribeの分割、組み合わせが可能です。

require "array_ext"

# 振る舞い、事後条件の記述
describe "empty array", :shared => true do
  it do
    @array.should be_empty
  end
end

describe "array", :shared => true do
  it "shoud append at last when send #<<" do
    a = "a"
    @array << a
    @array.last.should equal(a)
  end
end

# コンテキスト、事前条件の記述と、事後条件の組み合わせ
describe Array, "empty" do
  before(:each) do
    @array = []
  end

  it_should_behave_like "empty array"
  it_should_behave_like "array"
end

describe Array, "has items" do
  before(:each) do
    @array = [2,3,4]
  end

  it_should_behave_like "array"
end

事後条件セット側は、describeで、shared => :trueをし、事前条件用意側で、it_should_behave_like 「事後条件セット名」を呼ぶ必要があります。

また、itに名前を渡さない場合は、処理内容から自動生成されます。前記の場合、"should be empty"になるでしょう。

実行すると:

$ RUBYLIB=$RUBYLIB:lib/ spec ArraySpec.rb
...

Finished in 0.009816 seconds

3 examples, 0 failures

specチェックもひとつ増えました。

BDDしてみる

前述のArrayのついでに右から走査していくfoldrArrayのメソッドとして作ってみましょう。

require "array_ext"

# 振る舞い、事後条件の記述
describe "empty array", :shared => true do
  it do
    @array.should be_empty
  end
end

describe "array", :shared => true do
  it "shoud append at last when send #<<" do
    @array << "0"
    a = "a"
    @array << a
    @array.last.should equal(a)
  end
  it "should be iterate from last when send #foldr" do
    @array.foldr(@to_list_init, &@to_list_block).should == @to_list_result
  end
end

# コンテキスト、事前条件の記述と、事後条件の組み合わせ
describe Array, "empty" do
  before(:each) do
    @array = []
    @to_list_init = "nil"
    @to_list_block = proc do
      |a, b|
      "[" + a.to_s + "," + b + "]"
    end
    @to_list_result = "nil"
  end

  it_should_behave_like "empty array"
  it_should_behave_like "array"
end

describe Array, "has items" do
  before(:each) do
    @array = [2,3,4]
    @to_list_init = "nil"
    @to_list_block = proc do
      |a, b|
      "[" + a.to_s + "," + b + "]"
    end
    @to_list_result = "[2,[3,[4,nil]]]"
  end

  it_should_behave_like "array"
end

specでは[1,2,3,4].foldr("nil"){|l,r| "[" + l.to_s + "," + r + "]"}が期待する文字列"[2,[3,[4,nil]"になるべきだと書くことをします。

まず、@arrayだけでなく、foldr引数二つと結果もコンテキストで変わります。そのため、まず振る舞い側でそれらをすべてフィールドで渡すよう記述します。そして、コンテキスト側でbeforeでその値を用意します。

そしてとりあえず、lib/array_ext.rbを空にしてつくり、このままspec実行します

$ RUBYLIB=$RUBYLIB:lib/ spec ArraySpec.rb
..F.F

1)
NoMethodError in 'Array empty should be iterate from last when send #foldr'
undefined method `foldr' for []:Array
./ArraySpec.rb:17:

2)
NoMethodError in 'Array has items should be iterate from last when send #foldr'
undefined method `foldr' for [2, 3, 4]:Array
./ArraySpec.rb:17:

Finished in 0.011838 seconds

5 examples, 2 failures

foldrがないと言われています。で、array_ext.rbを以下のように書きます。

class Array
  def foldr(v, &block)
    v
  end
end

んで実行します。

$ RUBYLIB=$RUBYLIB:lib/ spec ArraySpec.rb
....F

1)
'Array has items should be iterate from last when send #foldr' FAILED
expected "[2,[3,[4,nil]]]", got "nil" (using ==)
./ArraySpec.rb:17:

Finished in 0.011083 seconds

5 examples, 1 failure

こんどは期待値と違うといわれます。

で、正しい動くコードをいれましょう。

class Array
  def foldr(v, &block)
    return v if empty?
    block.call(self[0], self[1..size].foldr(v, &block))
  end
end

で、実行すると

$ RUBYLIB=$RUBYLIB:lib/ spec ArraySpec.rb
.....

Finished in 0.011391 seconds

5 examples, 0 failures

と、成功しました。

次はfoldrメソッドをリファクタリングして、このままのspec実行して通ることもチェックすることを確認します。

class Array
  def rest
    if empty? then [] else self[1..size] end
  end
  def foldr(v, &block)
    return v if empty?
    block.call(first, rest.foldr(v, &block))
  end
  def foldl(v, &block)
    return v if empty?
    rest.foldl(block.call(v, first), &block)
  end
end

mockを使う

@to_list~というフィールドが3つできましたが、Mockオブジェクトを使えば以下のようにひとつにまとめられます。

  it "should be iterate from last when send #foldr" do
    @array.foldr(@to_list.init) do |a, b|
      @to_list.block(a, b)
    end.should == @to_list.result
  end

mockは以下の@to_listのように作ります。

describe Array, "empty" do
  before(:each) do
    @array = []
    @to_list = mock("to_list")
    @to_list.should_receive(:init).any_number_of_times.and_return("nil")
    @to_list.should_receive(:block).any_number_of_times do |a, b|
      "[" + a.to_s + "," + b + "]"
    end
    @to_list.should_receive(:result).any_number_of_times.and_return("nil")
  end

  it_should_behave_like "empty array"
  it_should_behave_like "array"
end

describe Array, "has items" do
  before(:each) do
    @array = [2,3,4]
    @to_list = mock("to_list")
    @to_list.should_receive(:init).any_number_of_times.and_return("nil")
    @to_list.should_receive(:block).any_number_of_times do |a, b|
      "[" + a.to_s + "," + b + "]"
    end
    @to_list.should_receive(:result).any_number_of_times.and_return("[2,[3,[4,nil]]]")
  end

  it_should_behave_like "array"
end

mockメソッドで名前指定でオブジェクトを作成します。mockはshould_receiveメッセージ名を指定し、呼び出され回数any_number_of_times(任意回)を指定し、戻り値を指定しています。呼び出され任意回にしたのは、emptyなどでもmockの実行回数チェックが起きてしまうからです。

同様のもので呼び出され回数をチェックしないものとしてstubもあるのですが、こちらはキーと値のペアだけになります(引数を受け付けるメッセージは作れない)。

mockの配置

入力は共通化されることから、mockは、振る舞い側で定義し、結果だけインスタンス側で定義するのがいいでしょう。

  it "should be iterate from last when send #foldr" do
    to_list = mock("to_list")
    to_list.should_receive(:block).exactly(@to_list_call).times do |a, b|
      "[" + a.to_s + "," + b + "]"
    end
    @array.foldr("nil") do |a, b|
      to_list.block(a, b)
    end.should == @to_list_result
  end

こうすることで、exactlyによって、チェックで使う実行回数を設定します。

実行回数、結果は、コンテキスト側に書きます。

describe Array, "empty" do
  before(:each) do
    @array = []
    @to_list_call = 0
    @to_list_result = "nil"
  end

  it_should_behave_like "empty array"
  it_should_behave_like "array"
end

describe Array, "has items" do
  before(:each) do
    @array = [2,3,4]
    @to_list_call = 3
    @to_list_result = "[2,[3,[4,nil]]]"
  end

  it_should_behave_like "array"
end

mockチェック要素を生成的に導く

@to_list_call@array.sizeそのものであり、@to_list_result@arrayの要素のループから生成できる。そこで、これらをitの側にうつしてやる。

require "array_ext"

describe "empty array", :shared => true do
  it do
    @array.should be_empty
  end
end

describe "array", :shared => true do
  it "shoud append at last when send #<<" do
    @array << "0"
    a = "a"
    @array << a
    @array.last.should equal(a)
  end
  it "should be iterate from last when send #foldr" do
    item_proc = proc do |a, b|
      "[" + a.to_s + "," + b + "]"
    end
    to_list = mock("to_list")
    to_list.should_receive(:block).exactly(@array.size).times(&item_proc)
    init = "nil"
    result = init
    @array.reverse.each do |item|
      result = item_proc.call(item, result)
    end

    #action
    @array.foldr(init) do |a, b|
      to_list.block(a, b)
    end.should == result
  end
end

describe Array, "empty" do
  before(:each) do
    @array = []
  end

  it_should_behave_like "empty array"
  it_should_behave_like "array"
end

describe Array, "has items" do
  before(:each) do
    @array = [2,3,4]
  end

  it_should_behave_like "array"
end

これはテストとその対象が近くにおける反面、チェックする結果の要素が直接的でなくなる。ただ、BDDは実装前に必ずspecチェックするので、結果的に失敗メッセージの中に結果値が埋め込まれるので、それでチェックできなくもない。

ただし、処理と結果導出があまりにもそっくりな場合、コード変更が同時影響すると、チェックは効かなくなるという面には注意したい。例ではfoldr自体もreverseを使って実装されていると、reverseが狂っている場合は、このspecチェックは多分通ってしまう。つまり、使用するメソッドは何らかの形で直接的なspecが入ってないといけないということになる。

rakeでspec実行するためのRakefile

rspecrake用taskを生成するライブラリも含んでいます。

たとえば、lib/以下においたライブラリ開発用のspecがspec/以下にある場合、そのspecを実行するtask runspecは以下のようにRakefileに書けばよいです。

begin
  gem "rspec"
  require 'spec/rake/spectask'
  desc "Run all specs"
  Spec::Rake::SpecTask.new('runspecs') do |t|
    t.libs << "lib"
    t.spec_files = FileList['spec/**/*.rb']
  end
ensure
end

rspecが入ってる環境なら

rake runspecs

で実行されます。

感想

BDD自体は、普通テストでも書き方を注意すればできることですが、このようにDSL風にすることでいろいろ機能を埋め込めるし、みやすいかもしれないです。

個人的に面白かったのは should be_emptyのようなDSL風メソッドの使い方でした。ブロックを使うことで、仕様に文字列を使えるのもいいです(そのかわりreturnが使えなくなるが)。

rspec自体についていえば、specコマンドruby同様の-Iオプションが使えればいいのにと思いました。bin/specを直接起動ではなく、rubyコマンドに食わせればいいのですが、それもださいし。

名前汚染の注意が必要

rspec-0.8.2では、specの中でグローバル変数classを定義すると、spec実行を同時に行うすべてのspecでそれらの名前が共有されるようです。

将来改善させるとは思いますが、現状spec中でそれらを行う場合は注意が必要です。

2007/04/12

[] ローカルコンテキスト持ちメソッド定義  ローカルコンテキスト持ちメソッド定義 - ラシウラ出張所 を含むブックマーク はてなブックマーク -  ローカルコンテキスト持ちメソッド定義 - ラシウラ出張所  ローカルコンテキスト持ちメソッド定義 - ラシウラ出張所 のブックマークコメント

rubyでは、defでメソッド定義した場合、defの外側にある変数def内では使えません。

family = "Yamada"

def hello(name)
  p "Hello " + name + " " + family
end

hello("taro") #=> familyに対し、NameErrorが出る

しかし、Module.define_methodを使うことで、ローカルコンテキストをバインドしたブロックを本体にしたメソッドの定義が可能になります。ただし、define_methodはprivateなので、instance_evalで呼び出す必要があります。

family = "Yamada"

self.class.instance_eval do
  define_method(:hello) do |name|
    p "Hello " + name + " " + family
  end
end

hello("Taro") #=> Hello Taro Yamada
family = "Tanaka"
hello("Jiro") #=> Hello Jiro Tanaka

この場合、mainオブジェクトののclassであるObjectにメソッドを追加してしまいます。

...
p Object.instance_methods.include? "hello" #=> true

このObjectの汚染を防ぐには、まず新たなModuleにメソッドを追加し、mainをそれでextendしてしまうのがいいでしょう。

family = "Yamada"

mod = Module.new
mod.instance_eval do
  define_method(:hello) do |name|
    p "Hello " + name + " " + family
  end
end
extend mod

hello("Taro") #=> Hello Taro Yamada
family = "Tanaka"
hello("Jiro") #=> Hello Jiro Tanaka
p Object.instance_methods.include? "hello" #=> false

この手法はどのobjectに対しても使用可能です。

この定義方法では、二段階のブロックを持ちます。instance_evalのdoとdefine_methodのdoです。

つまり、定義したメソッド内のコードの実行では、名前を以下の順に探していくことになります:

直上のブロックのみのコンテキストを保持するには、定義処理をメソッド化させてやればよいでしょう。

family = "Yamada"

def def_method(obj, name, &block)
  mod = Module.new
  mod.instance_eval do
    define_method(name, &block)
  end
  obj.extend mod
end

def_method(self, :hello) do |name|
  p "Hello " + name + " " + family
end

hello("Taro") #=> Hello Taro Yamada
family = "Tanaka"
hello("Jiro") #=> Hello Jiro Tanaka
p Object.instance_methods.include? "hello" #=> false