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