Born Too Late

Yuya's old tech blog.

MyObject クラスを少し改良?してみた

2010-02-28 06:26:20

昨日投稿したメタプログラミングで PHP をもっとオブジェクト指向にだと、以下のようにしか書けませんでした。

$obj = new MyObject(array('foo', 'bar', 'baz'));
echo $obj->join(',')->strtoupper();
// => FOO,BAR,BAZ

どういうことかというと、new の直後でメソッドを呼び出すことができないようで、上記のように 2 行で書かざるを得なくなっていました。

そこで、MyObject クラスを以下のように書き換えました。

class MyObject
{
    private $_selfMember;

    public function __construct($selfMember)
    {
        $this->_selfMember = $selfMember;
    }

    public static function init($selfMember)
    {
        return new self($selfMember);
    }

    public function __call($name, $args)
    {
        $new_args = array_merge(
            array($this->_getSelfMember()),
            $args
        );
        return new self(call_user_func_array($name, $new_args));
    }

    public function _getSelfMember()
    {
        return $this->_selfMember;
    }

    public function __toString()
    {
        return print_r($this->_selfMember, true);
    }
}

変更点は、init というスタティックメソッドを追加しただけです。

この変更により、以下のように書けるようになりました。

echo MyObject::init(array('foo', 'bar', 'baz'))->join(',')->strtoupper();
// => FOO,BAR,BAZ

楽しいような、危なっかしいような感じですが、言いたいこととしては、マジックメソッドを使うといろいろおもしろいことができそうですよ!ということにさせてください。こういう、いかにも役に立たなそう (というかむしろ害悪になりそう) なコードでも、遊びでいろいろ試しているうちに、実用に耐えうるアイディアに化けるかもしれません・・・。

メタプログラミングで PHP をもっとオブジェクト指向に

2010-02-27 13:30:17

PHP はブジェクト指向言語です。ですが、Ruby や JavaScript のような、より純度の高いオブジェクト指向言語とは違って、以下のようなことはできません。

puts ['foo', 'bar', 'baz'].join(',').upcase
# => FOO,BAR,BAZ
print(['foo', 'bar', 'baz'].join(',').toUpperCase());
// => FOO,BAR,BAZ

これは、Ruby や JavaScript において、配列や文字列がオブジェクトとして扱われ、メソッドを持つことができるからこそできる書き方です。

同じようなことを PHP でやろうとすると、以下のようなおぞましいことになってしまいます。

echo strtoupper(join(array('foo', 'bar', 'baz'), ','));
// => FOO,BAR,BAZ

Ruby や JavaScript のような、メソッドチェインを使った書き方と、PHP のような関数でラップしていく書き方で決定的に違うのは、やはり可読性でしょう。プログラムに書いた順番通りに処理されるので、ほとんどの人にとって、メソッドチェインの方が可読的であると考えられます。

そこで、PHP でも組み込み関数でメソッドチェインができるよう、以下のようなクラスを用意します。

class MyObject
{
    private $_selfMember;

    public function __construct($param)
    {
        $this->_selfMember = $param;
    }

    public function __call($name, $args)
    {
        $new_args = array_merge(
            array($this->_getSelfMember()),
            $args
        );
        return new self(call_user_func_array($name, $new_args));
    }

    public function _getSelfMember()
    {
        return $this->_selfMember;
    }

    public function __toString()
    {
        return print_r($this->_selfMember, true);
    }
}

このクラスを使うと、以下のようなコードが書けるようになります。

$obj = new MyObject(array('foo', 'bar', 'baz'));
echo $obj->join(',')->strtoupper();
// => FOO,BAR,BAZ

PHP の文法の制約上、ここまでが限界ですが、何とかそれらしくなりました。join も strtoupper も PHP の組み込み関数ですが、あたかもオブジェクトのメソッドのように呼び出すことができています。

この実装を可能にしているのが、PHP のマジックメソッドと呼ばれる、特殊なメソッドです。

__call メソッドは、メソッドが存在しない時に実行されます。引数として、実行しようとしたメソッドの名前と、その引数を受けるので、call_user_func_array に渡して、関数を実行しています。Ruby でいうと method_missing メソッドに相当します。

そして、上記のコードで、__call メソッドの返り値は、MyObject を new した新たなオブジェクトになっているので、このままでは、これを echo することはできません。

ここで、__toString を使うと、オブジェクトを文字列として出力するときの形式を定義できます。ここでは print_r を使っています。Ruby でいうと、to_s メソッドに相当します。 (ここでの動作は inspect メソッド的ですが)

ただし、この MyObject クラスは、あくまでも簡易的なものなので、存在しない関数を実行しようとしたときのエラー処理等をしていません。また、内部で実行する関数への、引数の渡し方によっては、うまく動作しません。例えば、explode や preg_replace など。

いろいろと問題点があって、MyObject 自信は使い物にはなりませんが、マジックメソッドによるメタプログラミングは、PHP によるプログラムの書き方の可能性を、大きく広げていると言えるでしょう。

だからと言って、チーム開発でこのようなコードを書くと、自分以外のメンバーに混乱を招くことになるので、安易に使うのはオススメできませんが・・・。

代入の返り値は代入値

2010-02-09 23:27:42

例えば memoize しながらフィボナッチ数列を求める場合。

class Fibonacci
  def initialize
    @memo = [0, 1]
  end

  def fibonacci(n)
    @memo[n] || @memo[n] = fibonacci(n - 2) + fibonacci(n - 1)
    @memo[n] # この行は省略しても結果は同じ
  end
end

fib = Fibonacci.new
10000.times { |n| fib.fibonacci(n) }

これで何が嬉しいのかというと、プロファイリングしてみるとこんな結果が現れます。

fibonacci メソッドの最後の行を省略しなかった場合

$ time ruby -rprofile fibonacci.rb
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 67.92     3.24      3.24    29996     0.11     0.21  Fibonacci#fibonacci
 16.98     4.05      0.81    59992     0.01     0.01  Array#[]
  6.29     4.35      0.30        1   300.00  4770.00  Integer#times
  3.56     4.52      0.17    19996     0.01     0.01  Fixnum#-
  2.73     4.65      0.13     9954     0.01     0.01  Bignum#+
  2.52     4.77      0.12     9998     0.01     0.01  Array#[]=
  0.00     4.77      0.00        2     0.00     0.00  Module#method_added
  0.00     4.77      0.00        1     0.00     0.00  Fibonacci#initialize
  0.00     4.77      0.00       45     0.00     0.00  Fixnum#+
  0.00     4.77      0.00        1     0.00     0.00  Bignum#coerce
  0.00     4.77      0.00        1     0.00     0.00  Class#inherited
  0.00     4.77      0.00        1     0.00     0.00  Class#new
  0.00     4.77      0.00        1     0.00  4770.00  #toplevel

real    0m6.005s
user    0m4.776s
sys     0m1.184s

fibonacci メソッドの最後の行を省略した場合

$ time ruby -rprofile fibonacci_fast.rb
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 66.39     2.39      2.39    29996     0.08     0.14  Fibonacci#fibonacci
  8.06     2.68      0.29    29996     0.01     0.01  Array#[]
  8.06     2.97      0.29    19996     0.01     0.01  Fixnum#-
  6.94     3.22      0.25        1   250.00  3600.00  Integer#times
  5.56     3.42      0.20     9998     0.02     0.02  Array#[]=
  5.00     3.60      0.18     9954     0.02     0.02  Bignum#+
  0.00     3.60      0.00        1     0.00     0.00  Fibonacci#initialize
  0.00     3.60      0.00        1     0.00     0.00  Class#inherited
  0.00     3.60      0.00        2     0.00     0.00  Module#method_added
  0.00     3.60      0.00        1     0.00     0.00  Class#new
  0.00     3.60      0.00       45     0.00     0.00  Fixnum#+
  0.00     3.60      0.00        1     0.00     0.00  Bignum#coerce
  0.00     3.60      0.00        1     0.00  3600.00  #toplevel

real    0m4.516s
user    0m3.600s
sys     0m0.904s

実行時間が約 25% 短縮されました。

注目するポイントは 2 行目の Array#[] メソッド、つまり配列から値を引き出す処理の回数が半分になっているというところ。どうしてこうなるかは、コードの意味を考えながら読めばすぐにわかると思います。ただ、こういう最適化によって、有意な効果を得られるケースがあるのかは疑問ですが・・・。

でもせっかくなので、代入の返り値はどうなってるのか、言語ごとに調べてみました。

Ruby

p (foo = "bar")
# => "bar"

JavaScript (Rhino)

var foo;
print(foo = "bar");
// => bar

Perl

my $foo;
print $foo = "bar";
# => bar

PHP

var_dump($foo = "bar");
// => string(3) "bar"

MySQL (これはちょっと番外編?)

mysql> SELECT @today := CURDATE();
+---------------------+
| @today := CURDATE() |
+---------------------+
| 2010-02-09          |
+---------------------+
1 row in set (0.00 sec)

と、ここまでは、どれも代入値が返ってくるようですが・・・

Python

print(foo = "bar")
# => SyntaxError: invalid syntax

Python では Syntax Error となります。

これを知って何の役に立つのかイマイチわかりませんが、こういうことになっていますよ、ということで。