Ruby でクロージャ

クロージャってブロックとしてよく使っているが、何者なのかよくわかってなかった。
急に気になったので調べてみた。ただし Wikipedia
クロージャ - Wikipedia

典型的には、クロージャはある関数全体が他の関数(以下、エンクロージャ)の内部で宣言されたときに発生し、内部の関数はエンクロージャのローカル変数(レキシカル変数)を参照する。実行時に外部の関数が実行された際、クロージャが形成される。クロージャは内部の関数のコードとエンクロージャのスコープ内の必要なすべての変数への参照からなる。

内部で宣言された関数は、宣言を行った関数内のローカル変数を参照できるってことかね。

サンプルコード

Wikipedia に 載ってた Javascript のカウンタを Ruby で書いてみた。

#!/usr/bin/env ruby
def count
  i = 0
  lambda {i += 1}
end

c1 = count
5.times do |i|
  puts c1.call
end
# >> 1
# >> 2
# >> 3
# >> 4
# >> 5


ちなみに関数の引数を利用することもできる。

#!/usr/bin/env ruby
def count i
  lambda {i += 1}
end

c1 = count 0
5.times do |i|
  puts c1.call
end

サンプルその2

これは間違いっぽい。
呼び出した関数のローカル変数ではなく、クラス内に値を保持してしまっているから。
やってることは似てるけど。

#!/usr/bin/env ruby
class Hoge
  def initialize
    @x = 0
  end
  def count
    return lambda{ @x += 1 }
  end
end

h = Hoge.new
5.times do |i|
  puts h.count.call
end

Ruby で Singleton

Ruby で Singleton を実装してみる。

モジュールなし

生成したオブジェクトをクラス変数へ保存しておく。

#!/usr/bin/env ruby
class Singleton
  @@instance = nil;

  def initialize
    puts "MyInitialize"
  end

  def self.instance
    @@instance = new if @@instance.nil?
    return @@instance
  end

  private_class_method :new
end

# Entry
a = Singleton::instance
b = Singleton::instance

if a === b
  puts "Singleton"
end

p [a, b]

a = Singleton.new
# 実行結果
$ ruby test.rb
MyInitialize
Singleton
[#, #]
test.rb:24: private method `new' called for Singleton:Class (NoMethodError)

Singleton モジュールを利用する

実装し終わったところで、 singleton パターンを提供するモジュールがあることを知った

#!/usr/bin/env ruby
require 'singleton'

class Single
  include Singleton
end

a = Single.instance
b = Single.instance

if a === b
  puts "Singleton!"
end

p [a,b]
a = Single.new
# 実行結果
Singleton!
[#, #]
test2.rb:17: private method `new' called for Single:Class (NoMethodError)

まとめ

車輪作る前にぐぐりましょう。

public なメソッドを private でオーバーライドした場合

Java だと確か、アクセス制限を弱める方向でオーバーライドできた覚えがある。
PHPRuby で同じことを試す機会があったのでメモ。

PHP の場合

Error になる。public なメソッドは public でないといけないらしい。
Java と似た方式。

<?php
class AbsTest {
  public function foo() {
    echo "foo";
  }
}

class Test extends AbsTest {
  private function foo() {
    echo "foo!";
  }

  public function bar() {
    self::foo();
  }
}

$a = new Test();
$a->bar();
// Fatal error: Access level to Test::foo() must be public (as in class AbsTest) 

Ruby の場合

アクセスを厳しくしてもOK。

#!/usr/bin/env ruby
#-*- coding:utf-8 -*-
class AbsTest
  def foo
    puts "foo"
  end
end

class Test < AbsTest
  def bar
    foo
  end
  
  private
  def foo
    puts "bar!"
  end
end

a = Test.new
a.bar #=> bar!

ちなみに Ruby のアクセス修飾子について

ちょっと調べてみたら、Ruby のアクセス制限はちょっと変わってるようだ。
以下引用。

public

そのメソッドが定義されたクラス内、サブクラス、クラス外(インスタンス)のどこからでもアクセス可能です。

private

メソッドは、クラスの内部(定義クラスとサブクラス)からのみアクセス可能。
レシーバを指定して呼び出すのは不可で、クラス内部からでもインスタンスメソッドとしては使えない。

protected

メソッドは、クラスの内部(定義クラスとサブクラス)からのみアクセス可能。
レシーバを指定して呼び出すことができ、クラス内部からであればインスタンスメソッドとして使える。

クラス定義でメソッドへのアクセス制限・public, private, protected by Ruby入門勉強ルーム

なるほどー。

Rubygemsがhomeディレクトリに入ってしまう

現象

gem list で表示されるのに、gem uninstall してもエラーになってしまう。

例えばgrowlnotifierを抜こうとする。

$ sudo gem uninstall growlnotifier                                [~]
ERROR:  While executing gem ... (Gem::InstallError)
    Unknown gem growlnotifier >= 0

原因

インストール時のPathが違っていた。homeディレクトリ下の.gemに入ってしまっていた。
gemが入っているpathを指定すれば、uninstallできる。

$ gem uninstall -i ~/.gem/ruby/1.8/ growlnotifier

なんでそんなとこに?

まずは自分の rubygem 環境。

$ gem env
RubyGems Environment:
  - RUBYGEMS VERSION: 1.3.1
  - RUBY VERSION: 1.8.7 (2008-08-11 patchlevel 72) [i686-darwin9]
  - INSTALLATION DIRECTORY: /opt/local/lib/ruby/gems/1.8
  - RUBY EXECUTABLE: /opt/local/bin/ruby
  - EXECUTABLE DIRECTORY: /opt/local/bin
  - RUBYGEMS PLATFORMS:
    - ruby
    - x86-darwin-9
  - GEM PATHS:
     - /opt/local/lib/ruby/gems/1.8
     - /Users/hoge/.gem/ruby/1.8
  - GEM CONFIGURATION:
     - :update_sources => true
     - :verbose => true
     - :benchmark => false
     - :backtrace => false
     - :bulk_threshold => 1000
     - :sources => ["http://gems.rubyforge.org/", "http://gems.github.com"]
  - REMOTE SOURCES:
     - http://gems.rubyforge.org/
     - http://gems.github.com


大抵は sudo gem install するので、下記のGEM PATHSにインストールされる。

 - /opt/local/lib/ruby/gems/1.8

しかし、sudo を付け忘れると、GEM PATHS のうちインストール可能なPATHに入れてしまうようだ。

 - /Users/hoge/.gem/ruby/1.8

こっちにインストールされてしまう。
しかも、 sudo gem uninstall するときには、/Users/... の方は見てくれないようだ。

まとめ

現象追っかけてみただけで、rubygemsのソースは見てません。
uninstall 時も GEM PATHS を全部検索してくれるといいのになー。

Capistrano のメイン開発者が燃え尽きたっぽい

お疲れ様でした。
ゆっくり休養して、また新しいアイデアを形にして欲しいですね。

But I’m burning out, and I have to drop these before things get worse.

the { buckblogs :here }: Net::SSH, Capistrano, and Saying Goodbye



ってよく見たら Capistrano だけじゃなく、SQLite 関係のライブラリとかも作ってたのね。

Secondly: I’m ceasing development on SQLite/Ruby, SQLite3/Ruby, Net::SSH (and related libs, Net::SFTP, Net::SCP, etc.) and Capistrano. I will no longer be accepting patches, bug reports, support requests, feature requests, or general emails related to any of these projects.

the { buckblogs :here }: Net::SSH, Capistrano, and Saying Goodbye


いい機会だから、ソースコードでも読んでみよう。

Rubyの誕生日

昨日、2月24日が Ruby の誕生日だったようです。

Rubyは1993年2月24日に生まれました。その日同僚とオブジェクト指向 言語の可能性について話していました。

FAQ::一般的な質問 - Rubyリファレンスマニュアル

1993年2月24日は「Rubyの誕生日」ということになっている。これは『オブジェクト指向スクリプト言語Ruby』の共著者でもある石塚さんと、新しいオブジェクト指向言語を作ることについて最初に話しあった日であり、「Ruby」という名前が決まった日でもある。

Matzにっき(2005-02-24)

話し合いの様子はこちらで引用されています。
今日はRubyの誕生日。ということでRubyの言語名決定の経緯 - OneRingToFind by 榊祐介


16歳の誕生日を 1.9.1 という新しいバージョンで迎えられて、なによりですね。
おめでとうございます!

Twitter4rのエラーを追って見る

普段、twitter には twitter4r を使った投稿用のスクリプトで投げている。仕事中にぼやきたい時などにターミナルから直接投げれるのでちょっと便利*1
そんなわけでタイムラインはあまり見ていない。投稿時に10行だけ表示されるようにしているので、チラっと見るくらい。

さて、先日たまたま定期メンテ時に投稿しようとしたらエラー吐き出したので、何が起こっているのかソースを追ってみた記録。

いきなりのエラー

/ruby/gems/1.8/gems/twitter4r-0.3.0/lib/twitter/client/timeline.rb:68:in `timeline_for': undefined method `each' for :Twitter::Status (NoMethodError)

さっきまで普通に投げられたのに、突然壊れたかと思ったら、Twitterがメンテ中だった。

メンテだとしても、もう少し気の利いたエラーを出して欲しいものだと思いつつ、ちょっと twitter4r ソースを見てみた。

twitter4r

エラー箇所のソース @ twitter4r

# /twitter4r-0.3.0/lib/twitter/timeline.rb

  def timeline_for(type, options = {}, &block)
    raise ArgumentError, "Invalid timeline type: #{type}" unless @@TIMELINE_URIS.keys.member?(type)
    uri = @@TIMELINE_URIS[type]
    response = http_connect {|conn| create_http_get_request(uri, options) }
    timeline = Twitter::Status.unmarshal(response.body)
    timeline.each {|status| bless_model(status); yield status if block_given? }
    timeline
  end

メンテ中でもHTTPステータスコードは200らしい。http_connection はresponseを返す。

# /twitter4r-0.3.0/lib/twitter/model.rb

def unmarshal(raw)
    input = JSON.parse(raw)
    def unmarshal_model(hash)
      self.new(hash)
    end
    return unmarshal_model(input) if input.is_a?(Hash) # singular case
    result = []
    input.each do |hash|
      model = unmarshal_model(hash) if hash.is_a?(Hash)
      result << model
    end if input.is_a?(Array)
    result # plural case
end

Twitter::Status.unmarshal(response.body) は、通常JSONをパースした Hash から生成された Twitter::Status のインスタンスを返す。
このインスタンスの中身が nil になってしまうわけか。

response.body に JSON ではなく login 辺りが返されてるとして、JSON.parse は一体何を返してくるんだ?

json

gem の依存で json-1.1.3 が入っていることと、twitter.rb で

require 'json'

とかなってるので gem を使っていると解釈。

parser.rb のソースは読んでみたが、ひたすら case 文とかそんな感じなので省略。
実行結果。

JSON.parse('{"name": "hoge"}') #=> {"name"=>"hoge"}
JSON.parse('{}') #=> {}
JSON.parse('["foo","bar"]') #=> ["foo", "bar"]
JSON.parse('[]') #=> []

json gem は大変お行儀がいいのか、ちゃんと例外出してくれます。わかりやすい。
ここでエラーが吐かれていないので、やはり Hash か Array を返しているのだろう。

Twitter::Status

では、nil っぽいものを返してくる Twitter::Status を見てみる。
Hash を受け取って self.new してるわけだが、コンストラクタ定義がない。

  class Status
    include ModelMixin
    @@ATTRIBUTES = [:id, :text, :created_at, :user]
  ...

引数受け取れないんじゃね?と思っていたら、include している ModelMixin が include している ClassUtilMixin に initialize が定義されていた。

  module ClassUtilMixin #:nodoc:
    def self.included(base) #:nodoc:
      base.send(:include, InstanceMethods)
    end
    
    # Instance methods defined for <tt>Twitter::ModelMixin</tt> module.
    module InstanceMethods #:nodoc:
    (中略)
      def initialize(params = {})
        params.each do |key,val|
          self.send("#{key}=", val) if self.respond_to? key
        end
        self.send(:init) if self.respond_to? :init
      end
  (略)

params内のkey-valueをそのまま対象のクラスに送り込み、インスタンス変数として使用できるようにしている。
APIのパラメター名が attr_accessor にセットされているので、外からアクセス可能なインスタンス変数ができあがる。

投了

で、無事 Twitter::Status.new(hash) なインスタンスが返される。
ここで問題がもう一つ。

 timeline.each {|status| bless_model(status); yield status if block_given? }

どこで Enumable 使用できるようにしているのか、全く掴めない・・・
力つきました。

そんなわけで、

  • Twitter メンテ時の timeline の JSON が不明。
  • Twitter::Status がどうやって each 使えるようになっているのか?

という2点を疑問として残し、投了です。

おまけ

#!/usr/bin/env ruby
# -*- coding:utf-8 -*-
require 'rubygems'
require 'twitter'
require 'time'

twitter = Twitter::Client.new
public_timeline = twitter.timeline_for(:public, :count => 1) do |status|
  p status.methods
end

#=>["inspect", "tap", "clone", "created_at=", "public_methods", "object_id", "__send__", "text", "instance_variable_defined?", "init", "equal?", "freeze", "to_json", "text=", "extend", "send", "j", "methods", "to_yaml_properties", "hash", "dup", "to_enum", "to_yaml", "taguri", "instance_variables", "require_block", "eql?", "taguri=", "jj", "instance_eval", "id", "to_i", "singleton_methods", "client", "id=", "taint", "user", "frozen?", "instance_variable_get", "enum_for", "client=", "bless", "to_yaml_style", "instance_of?", "display", "to_a", "method", "user=", "type", "instance_exec", "protected_methods", "==", "===", "instance_variable_set", "basic_bless", "respond_to?", "kind_of?", "to_s", "class", "to_hash", "private_methods", "=~", "tainted?", "__id__", "JSON", "untaint", "nil?", "is_a?", "created_at"]

*1:ぼーっとながめる時は termtter と夏ライオン