Ruby のメソッド間で変数を共有する

Ruby のメソッド間では、ふつうは変数は共有されません。

def counter_set
  x = 0
end

def inc
  x += 1
end

def counter_value
  x
end

counter_set
inc
inc
counter_value

は最初の inc 呼び出しのところで

undefined method `+' for nil:NilClass (NoMethodError)

のエラーが出ます。これは Ruby の仕様で、却ってメソッドは安全ともいえるわけです。もし変数を共有したければ、メソッドではなく、クロージャである proc を使えば事足ります。

x = 0

inc = proc do
  x += 1
end

inc.call
inc.call
x          #=>2

という具合に。


しかし、これでは「イヤだ」、メソッドを使いたいと仰るわがままな方もおられましょう。これはじつは、Rubyメタプログラミングを使えば可能です。

def counter_set
  x = 0
  
  define_method(:inc) do
    x += 1
  end
  
  define_method(:counter_value) do
    x
  end
end

counter_set
inc
inc
counter_value    #=>2

こんな具合ですね。Module#define_method のメソッド定義がブロック(Ruby のブロックはクロージャです)で行われているために、変数 x が透過して保持されます。

同様のことは、メソッドだけでなくクラスやモジュールでも可能です。


メタプログラミングRuby 第2版

メタプログラミングRuby 第2版


なお、上のはなんちゃってメソッドですが、もし冗談にせよ使うなら、こんな感じの方がいいかも知れません。

module Kernel
  def counter_set
    x = 0
    Kernel.send(:define_method, :inc) do
      x += 1
    end
    
    Kernel.send(:define_method, :counter_value) do
      x
    end
  end
end

これなら、クラスの中でなど、どこでも使えます。例えばこんな感じ。

counter_set

class A
  inc
  def initialize
    inc
  end
end

counter_value    #=>1
A.new
counter_value    #=>2

GTK+ で落書き 7(Ruby)

Gem 'oekaki' で落書きです。
oekaki | RubyGems.org | your community gem host
GTK+でお絵かきしてみた(Ruby) - Camera Obscura


引き続き Tool#star を使っています。

 

require 'oekaki'

Oekaki.app do
  draw do
    color(0, 0, 0)
    rectangle(true, 0, 0, 300, 300)
  end
    
  po = Vector[0, 150]
  θ = PI / 50
  a = Matrix[[cos(θ), -sin(θ)], [sin(θ), cos(θ)]]
  
  timer(20) do
    color(0, 0, 0)
    rectangle(true, 0, 0, 300, 300)
    color(0, 65535, 0)
    star(false, 150, 150, 150 + po[0], 150 - po[1])
    po = a * po
  end
end

オブジェクト指向じゃんけんゲームの Ruby 移植版

前回エントリの C# バージョンの、Ruby 移植版です。
 

module JankenAsobi
  class Player
    def initialize
      @wincount = 0
    end
    attr_reader :wincount, :name
    
    def count
      @wincount += 1
    end
  end

  class Cp < Player
    def initialize
      @name = "Computer"
      super
    end
    
    def show_hand
      rand(3)
    end
  end
  
  class Man < Player
    def initialize
      print "あなたの名前を入力して下さい:"
      @name = gets.chomp
      @name = "名無し" if !@name or @name.empty?
      super
    end
    
    def show_hand
      begin
        print "#{@name}の手を入力して下さい(0:グー, 1:チョキ, 2:パー):"
        n = gets.chomp
      end until n == "0" or n == "1" or n == "2"
      n.to_i
    end
  end

  class Judge
    Hand = ["グー", "チョキ", "パー"]
    
    def initialize(p1, p2)
      @player1 = p1
      @player2 = p2
      puts "#{@player1.name} 対 #{@player2.name} :じゃんけん開始\n "
    end
    
    def game(n)
      puts "*** #{n}回戦 ***"
      hand1 = @player1.show_hand
      hand2 = @player2.show_hand
      judgement(hand1, hand2)
    end
    
    def judgement(h1, h2)
      winner = @player1
      print "#{Hand[h1]} 対 #{Hand[h2]}で "
      if h1 == h2
        puts "引き分けです。"
        return
      elsif (h1 - h2) % 3 == 1
        winner = @player2
      end
      puts "#{winner.name}の勝ちです。"
      winner.count
    end
    private :judgement
    
    def winner
      p1 = @player1.wincount
      p2 = @player2.wincount
      finalwinner = @player1
      print "\n*** 最終結果 ***\n#{p1}#{p2} で "
      if p1 == p2
        puts "引き分けです。"
        return
      elsif p1 < p2
        finalwinner = @player2
      end
      puts "#{finalwinner.name}の勝ちです。"
    end
  end

  def self.play
    player1 = Cp.new
    player2 = Man.new
    judge = Judge.new(player1, player2)
    5.times {|i| judge.game(i + 1)}
    judge.winner
  end
end

JankenAsobi.play

GTK+ で落書き 6(Ruby)

Gem 'oekaki' で落書きです。
oekaki | RubyGems.org | your community gem host
GTK+でお絵かきしてみた(Ruby) - Camera Obscura


スターを描いてみました。

 

require 'oekaki'

Oekaki.app do
  draw do
    color(0, 0, 0)
    rectangle(true, 0, 0, 300, 300)
    
    color(0xdc00, 0xdc00, 0xdc00)     #gainsboro
    arc(true, 0, 0, 300, 300, 0, 64 * 360) 
    color(0x6b00, 0x8e00, 0x2300)     #olivedrab
    star(true, 150, 150, 150, 0)
  end
end

メソッド Tool#star(fill, x1, y1, x2, y2, color = nil) を新たに書きました。fill は ture で塗りつぶし、false で輪郭のみ描画します。(x1, y1) はスターの中心、(x2, y2) は(とがった)頂点のひとつの座標です。


こんなのも。

 

require 'oekaki'

Oekaki.app width: 400, height: 400 do
  draw do
    color(0, 0, 0)
    rectangle(true, 0, 0, 400, 400)
    
    color(0, 65535, 0)
    po = Vector[0, 50]
    θ = PI / 15
    a = Matrix[[cos(θ), -sin(θ)], [sin(θ), cos(θ)]]
    for y in 0..3
      for x in 0..3
        x1 = x * 100 + 50
        y1 = y * 100 + 50
        star(false, x1, y1, x1 + po[0], y1 - po[1])
        po = a * po
      end
    end
  end
end

Gem の内部で require 'matrix', include Math をしているので、それらのクラスとモジュールがそのまま使えます。
 
 
Gem 'oekaki' に加えたコードはだいたいこんな感じです。

require 'matrix'
include Math

module Oekaki
  class Tool
    def star(fill, x1, y1, x2, y2, color = nil)
      set_color(color)
      Star.new(fill, x1, y1, x2, y2, @color).draw
    end
  end

  class Star < Tool
    def initialize(fill, x1, y1, x2, y2, color)
      @fill = fill
      @o = []; @a = []; @b = []
      @o[0], @o[1] = x1, y1
      @a[0] = Vector[x2 - x1, y1 - y2]
      
      θ = PI / 5
      rt1 = Matrix[[cos(θ), -sin(θ)], [sin(θ), cos(θ)]]
      rt2 = rt1 * rt1
      1.upto(4) {|i| @a[i] = rt2 * @a[i - 1]}
      
      t = cos(2 * θ) / cos(θ)
      @b[0] = rt1 * @a[0] * t
      1.upto(4) {|i| @b[i] = rt2 * @b[i - 1]}
      
      super()
      @color = color
    end
    
    def draw_triangle(n)
      ar = [@a[n].to_w(@o), @b[n].to_w(@o), @b[(n - 1) % 5].to_w(@o)]
      polygon(@fill, ar)
    end
    private :draw_triangle
    
    def draw
      if @fill
        5.times {|i| draw_triangle(i)}
        ar = []
        5.times {|i| ar << @b[i].to_w(@o)}
        polygon(@fill, ar)
      else
        ar = []
        5.times {|i| ar << @a[i].to_w(@o); ar << @b[i].to_w(@o)}
        polygon(@fill, ar)
      end
    end
  end
end

class Vector
  def to_w(o)
    v = self
    [o[0] + v[0], o[1] - v[1]]
  end
end

スターのそれぞれの頂点は、頂点ベクトルに回転行列(変数 rt2)を順に掛けて求めています。Ruby では標準添付ライブラリの 'matrix' で行列やベクトルの計算が簡単に行なえます。

一番苦心したのがじつは色の取り扱いです。Tool#star メソッドの中で Starクラスのインスタンスを新たに作っているので、Tool#color を Tool#star に対して使うとき、Starクラスは Toolクラスを継承しているものの、Toolクラスのインスタンス変数の内容が Star.new で上書きされてしまいます。なので、少しトリッキーなことをしています。


「Gem 'oekaki'」タグを作りました。

ファイルが画像でなければ削除する(Ruby)

削除すれば true、しなければ false を返します。なお、判断は拡張子で行っているわけではありません。

irb(main):007:0> require './delete_non_img'
=> true
irb(main):008:0> Utils.delete_non_img("img.png")    #削除しない
=> false
irb(main):009:0> Utils.delete_non_img("text")       #削除する
=> true


delete_non_img.rb

require 'fileutils'
require 'utils'

module Utils
  def delete_non_img(fname)
    if Utils.imgexist?(fname)
      false
    else
      FileUtils.rm(fname)
      true
    end
  end
  module_function :delete_non_img
end

モジュール 'utils' は自家製です(参照)。

GTK+ でプログレスバーを簡単に使う(Ruby)

長い処理だと、プログレスバーがあると便利ですよね。GTK+ で簡単に表示できるライブラリを作ってみました。

 

使い方はこんな感じです。

Utils.progress_bar do |bar|
  for i in 0..20
    bar.fraction = i / 20.0
    bar.text = "#{i * 5}%"
    sleep(0.5)
  end
end

Utils.progress_bar のブロック内に処理を書きます。ブロック変数 bar のインスタンス・メソッド Gtk::ProgressBar#fraction= あるいは Gtk::ProgressBar#set_fraction()プログレスバーの値を決めるもので、0.0〜1.0 の値を代入して下さい。ウィンドウは自動で消えないので、ウィンドウに付いている消去のマークをクリックして処理が完了します。なお、ウィンドウを強制消去してしまった場合はユーザーの処理も中断してしまうので、注意して下さい。

なお、Gtk::PorgressBarインスタンス・メソッドはすべて使うことができます(text= なども)。


Ruby コード。

require 'gtk2'

module Utils
  def progress_bar
    w = Gtk::Window.new
    w.signal_connect("destroy") {Gtk.main_quit}
    w.set_size_request(300, 50)
    w.border_width = 10
    w.title = "Progress"
    
    bar = Gtk::ProgressBar.new
    w.add(bar)
  
    Thread.new(bar) do |bar|
      yield(bar)
    end
      
    w.show_all
    Gtk.main
  end
  module_function :progress_bar
end

なお、これは 野良 Gem 'Utils' - Camera Obscura に入れてあります。


※参考
gtk2-tut-progressbar - Ruby-GNOME2 Project Website

GTK+ で落書き 5(Ruby)

Gem 'oekaki' で落書きです。
oekaki | RubyGems.org | your community gem host
GTK+でお絵かきしてみた(Ruby) - Camera Obscura


キャンバス空間と色空間をリニアに連続的変化させて、ドットを描いています。


Ruby コード。

require 'oekaki'
require 'matrix'

L = 400
C_MAX = 65536
C_STEP = 1000

class Field
  def initialize
    @x = rand(L); @y = rand(L)
    n = Vector[rand, rand]
    @n = [n[0] * 2 / n.norm, n[1] * 2 / n.norm]
  end
  attr_reader :x, :y

  def next
    @x, @y = @x + @n[0], @y + @n[1]
    @n[0] = -@n[0] if @x < 2 or @x >= L - 2
    @n[1] = -@n[1] if @y < 2 or @y >= L - 2
  end
end

class Color
  def initialize
    init
  end
  attr_reader :x, :y, :z
  
  def init
    @x = rand(C_MAX); @y = rand(C_MAX); @z = rand(C_MAX)
    cn = Vector[rand, rand, rand]
    @cn = [cn[0] / cn.norm, cn[1] / cn.norm, cn[2]/ cn.norm]
  end
  
  def next
    @x, @y, @z = @x + @cn[0] * C_STEP, @y + @cn[1] * C_STEP, @z + @cn[2] * C_STEP
    @cn[0] = -@cn[0] if @x < C_STEP or @x >= C_MAX - C_STEP
    @cn[1] = -@cn[1] if @y < C_STEP or @y >= C_MAX - C_STEP
    @cn[2] = -@cn[2] if @z < C_STEP or @z >= C_MAX - C_STEP
    init if rand < 0.001
  end
end

pos = Field.new
col = Color.new

Oekaki.app width: L, height: L do
  draw do
    color(0, 0, 0)
    rectangle(true, 0, 0, L, L)
  end
  
  timer(20) do
    color(col.x.to_i, col.y.to_i, col.z.to_i)
    arc(true, pos.x.to_i + 2, pos.y.to_i + 2, 4, 4, 0, 64 * 360)
    pos.next
    col.next
    true
  end
end

だいぶ DRY原則に反したコードですが、お許しを。