PHP
複素数

PHPに演算子のオーバーロードが欲しくなったのでPHPに導入してみた

こんにちはみなさん

タイトルを見たとき、ほとんどの人が最後の一節にツッコミを入れるでしょう。
「PHPでやるなよ」とね。
しかしながら、「複素数計算とか行列計算とか、もっと直感的に書きたいなぁ、PHPで」って、考えちゃうのはやはりPHPerの性だと思うんですよね。
たとえ、Python 使ったほうが手っ取り早くとも、PHPの可能性を追求する上ではありなんじゃないかって思うのです。

...ありだよね?

演算子のオーバーロード

オーバーロード

オーバーロードと言うのは、関数や演算子が、引数の型などに応じて、振る舞いを変えるようなことをいいます。
多重定義とかwikipediaには書いてあります。

演算子のオーバーロード

演算子の右側の値も、その演算子の引数とみなせます。演算子の右側の値の型によって挙動が変わる用に作ると、演算子のオーバーロードが実現できます。

演算子のオーバーロードの超身近な例といえば、pythonにおける演算子があります。
pythonで例えば整数に整数をかけた場合と、文字列に整数をかけた場合というものを見てみましょう。

>>> 3 * 2
6
>>> 3 * 'abc'
'abcabcabc'
>>> 'abc' * 3
'abcabcabc'

このように、整数がかけられる相手が整数なのかリストなのかで挙動が大きく変わり、返却される方も変わっていることがわかります。

PHPでのオーバーロード

PHPのオーバーロードはあるにはあるのですが、私の知っているオーバーロードから外れちゃっているように思います。
http://php.net/manual/ja/language.oop5.overloading.php

PHPで演算子のオーバーロードをする

というわけで、さっさとやっていきましょう。
なんか、RFCにあるにはあるのですが、未だドラフトのままです。
なので、素のPHPには、演算子のオーバーロードはありません。
エクステンションを利用して演算子のオーバーロードを導入する必要があります。

エクステンションの導入

今回はこれを使います。
https://github.com/php/pecl-php-operator
peclって書いてありますが、peclではインストール出来ないので、自力でビルドする必要があります。

自信のネイティブなphp環境とかに(おそらく使わないだろう)このエクステンションを入れるのもあれなので、例によってDockerイメージにしましょう。

FROM php

RUN apt-get update && apt-get install -y git && \
git clone https://github.com/php/pecl-php-operator.git temp && \
cd temp && \
phpize && ./configure && make && make install &&\
docker-php-ext-enable operator

WORKDIR /var/www

CMD ["bash"]

こいつを適当なイメージで保存してしまえばオッケーです。

docker build -t niisan/php:overload .

operator エクステンション

このエクステンションは、演算子のオーバーロードをマジックメソッドを使う形で実現します。
全部書くと冗長なのでそれは本家のリポジトリの方に任せますが、ここでは主要なものだけ見てみましょう。

<?php
class Something
{
    public function __add($arg){}// $this + $arg
    public function __sub($arg){}// $this - $arg
    public function __mul($arg){}// $this * $arg
    public function __div($arg){}// $this / $arg
}

こんな感じでマジックメソッドを定義すると、自信に対する四則演算に対して、任意の動きを定義することができます。

複素数の例

直感的にオーバーロードの効果を実感するためには、複素数を例に取るのが手っ取り早いです。
複素数は実部と虚部に別れた数のことで

z = a + bi \ \ \ ( i = \sqrt{-1} )

で表されます。ここで$bi$は虚部と呼ばれ、$i^2 = -1$という性質を持っています
複素数は普通の実数と同じように計算が定義できて、

z_1 = a_1 + b_1 i \ \ ,  \ \ z_2 = a_2 + b_2 i \\
z_1 \pm z_2 = (a_1 \pm a_2) + (b_1 \pm b_2)i \\
z_1 z_2 = (a_1 a_2 -b_2 b_2) + (a_1b_2 + a_2b_1)i

となります。

複素数をPHPのクラスで表すと、例えば

<?php

class Complex
{
    private $real;
    private $img;

    public function __construct($real = 0, $img = 0)
    {
        $this->real = $real;
        $this->img = $img;
    }

    public function __toString()
    {
        return "$this->real + i$this->img";
    }
}

$test = new Complex(3, 4);
echo $test . "\n";

こいつを実行すると

3 + i4

となります。

更に足し算をしてしまいましょう。

$z1 = new Complex(2, 3);
$z2 = new Complex(5, 7);

$z3 = $z1 + $z2;
echo $z3 . "\n";// >> 2

個人的にはエラーを出してほしいところですが、そもそも足し算定義していないので、なにか妙なものが返ってきたというところでしょう。
しかし、数式と同じように、コード上でも演算を表現できれば、コードと数式がより直感的に結びつけることができるはずです。
つまり、

$z3 = $z1 + $z2
echo $z3 . "\n";// >> 7 + i10

となってくれればよいわけです。

オーバーロードで四則演算を定義

こんな時こそオーバーロードの出番です。
試しに、以下のマジックメソッドを定義してみます。

// Complex 内
    public function __add($arg)
    {
        if (is_int($arg) or is_float($arg)) {
            $arg = new self($arg);
        }

        return new self($this->real + $arg->real, $this->img + $arg->img);
    }

    public function __get($name)
    {
        if (isset($this->{$name})) {
            return $this->{$name};
        }

        return null;
    }

ここで__add()というのがエクステンションによって追加されたマジックメソッドで、演算子のオーバーライドをします。引数の$argは演算子の右側にある引数になります。
また、__getはもとからあるマジックメソッドで、プロパティをread only にするために使用しています。

こんな感じでマジックメソッドを定義してやると、先の結果はちゃんと 7 + i10になります。
同様に四則演算を定義してやると、

$x = new Complex(1, 1);
$y = new Complex(2, 3);

function ep($str)
{
    echo $str . "\n";
}

ep($x);// >> 1 + i1
ep($y);// >> 2 + i3
ep($x + $y);// >> 3 + i4
ep($x + 1);// >> 2 + i1
ep($y - $x);// >> 1 + i2
ep($x * 2);// >> 2 + i2
ep($x * $y);// >> -1 + i5
ep($y / $x);// >> 2.5 + i0.5
ep($y / 2);// >> 1 + i1.5
ep(1 + $x);// >> 2

コードは最後に全部乗っけます

比較演算子のオーバーロード

オブジェクトの比較は、基本的には比較対象が「同じ」かどうかを調べます。
これは、決して数値が同じかどうかを意味しません。
例えば

$a = new Complex(2, 3);
$b = new Complex(2, 3);

var_dump($a === $b);// >> bool(false)

これは、参照しているオブジェクトが違うため、正しくないという判断になっている模様です。
ちなみに == は文字列表現が等しいためかtrueという結果を返してくれます。

しかし、今、複素数に対しては数としての役割以上を求めておらず、こいつの格納先がどこにあるかとか興味はないわけです。
そこで、比較演算子のオーバーロードをしてみます。

    /**
     * === のオーバーロード
     */
    public function __is_identical($arg)
    {
        return $this->operationTemplate($arg, function(self $a) {
            return $this->real === $a->real and $this->img === $a->img;
        });
    }

    /**
     * 複素数に直せるものは複素数に直してから処理する
     */
    private function operationTemplate($arg, callable $callback)
    {
        $arg = $this->realToComplex($arg);
        if($arg instanceof self) {
            return $callback($arg);
        }

        throw new \RuntimeException('This arg type invalid');
    }

    /**
     * 実数を複素数に直す
     */
    private function realToComplex($arg)
    {
        if (is_int($arg) or is_float($arg)) {
            $arg = new Complex($arg);
        }

        return $arg;
    }

こんな感じのものを作ってみました。
ゴチャついてますが、はじめのメソッドがメインになってます
こうすると次のようになります。

$a = new Complex(2, 3);
$b = new Complex(2, 3);

var_dump($a === $b);// >> bool(true)
var_dump($a * 2 === $b * 2);// >> bool(true)
var_dump(new Complex(1) === 1);// >> bool(true)
var_dump(new Complex(0, 1) === 1);// >> bool(false)

便利ですね。

今回のコードの全体

結局、最終的にどういうコードになったかは以下のとおりです。

<?php
namespace Niisan;

class Complex
{

    private $real;
    private $img;

    public function __construct(float $real = 0, float $img = 0)
    {
        $this->real = $real;
        $this->img  = $img;
    }

    /**
     * 複素数の足し算をし、結果を新しいインスタンスとして返却
     *
     * @param self $com
     * @return self
     */
    public function add(self $com): self
    {
        $real = $this->real + $com->real;
        $img  = $this->img  + $com->img;
        return new self($real, $img);
    }

    /**
     * 複素数を掛け、計算結果を新しいインスタンスとして返却
     *
     * @param self $com
     * @return self
     */
    public function mul(self $com): self
    {
        $real = $this->real * $com->real - $this->img * $com->img;
        $img  = $this->real * $com->img + $this->img * $com->real;
        return new self($real, $img);
    }

    /**
     * 逆を取得
     *
     * @return self
     */
    public function reverse(): self
    {
        return new self(-$this->real, -$this->img);
    }

    /**
     * 複素共役を取得
     *
     * @return self
     */
    public function conjugate(): self
    {
        return new self($this->real, -$this->img);
    }

    /**
     * 絶対値を取得
     *
     * @return float
     */
    public function abs(): float
    {
        return sqrt(($this * $this->conjugate())->real);
    }

    /**
     * 足し算( + )のオーバーロード
     */
    public function __add($arg)
    {
        return $this->operationTemplate($arg, function(self $a) {
            return $this->add($a);
        });
    }

    /**
     * 引き算 ( - )のオーバーロード
     */
    public function __sub($arg)
    {
        return $this->operationTemplate($arg, function(self $a) {
            return $this->add($a->reverse());
        });
    }

    /**
     * 掛け算 ( * ) のオーバーロード
     */
    public function __mul($arg)
    {
        return $this->operationTemplate($arg, function(self $a) {
            return $this->mul($a);
        });
    }

    /**
     * 割り算 ( / ) のオーバーロード
     */
    public function __div($arg)
    {
        return $this->operationTemplate($arg, function(self $a) {
            $target = new self($a->real / ($a->abs() ** 2), -$a->img / ($a->abs() ** 2));
            return $this->mul($target);
        });
    }

    /**
     * === のオーバーロード
     */
    public function __is_identical($arg)
    {
        return $this->operationTemplate($arg, function(self $a) {
            return $this->real === $a->real and $this->img === $a->img;
        });
    }

    public function __toString()
    {
        return "{$this->real} + i$this->img";
    }

    public function __get($arg)
    {
        if (isset($this->{$arg})) {
            return $this->{$arg};
        }

        return null;
    }

    /**
     * 実数を複素数に直す
     */
    private function realToComplex($arg)
    {
        if (is_int($arg) or is_float($arg)) {
            $arg = new Complex($arg);
        }

        return $arg;
    }

    /**
     * 複素数に直せるものは複素数に直してから処理する
     */
    private function operationTemplate($arg, callable $callback)
    {
        $arg = $this->realToComplex($arg);
        if($arg instanceof self) {
            return $callback($arg);
        }

        throw new \RuntimeException('This arg type invalid');
    }


}

まあ、特に難しそうなところはないですね。

まとめ

というわけで、演算子のオーバーロードをPHPでやってみました。
いやーなんでこれ、採用されていないんでしょうねぇー、不思議ですねぇー
...
まあ、これ、使い所あるかと言われると、非常に限定的と言わざるを得ず、ほしい時にエクステンションで入れてくれや、ってことだと思うので、PHP本体に入れなくていいようには思います。
ところでエクステンションのソース見ると、エラい短いんですよね。
これなら、numpyみたいな機構をエクステンションで実装することも...

今回はこの辺にしておきます。