なんか考えてることとか

変な人が主にプログラミング関連で考えていることをまとめる。

PHPで始めるプロトタイプベースOOP (後編)

前編からの続き。
opaupafz2.hatenablog.com

前作: Pythonで始めるプロトタイプベースOOP
opaupafz2.hatenablog.com
opaupafz2.hatenablog.com

メソッドスロットの定義

次にPHPに対してメソッドスロットを定義していきたい。幸いにも、PHPでは5.3以降で"途中式を含む"クロージャを代入することができる*1ので、メソッドスロット自体は簡単に実現可能である。

<?php
require_once "parentobject.php";

$obj = new ParentObject();  // ペアレントを生成
$obj->name = "PHP";         // スロットを定義

// メソッドスロットの定義
$obj->hello = function() use ($obj) {
    echo "Hello, $obj->name.", PHP_EOL;
};
?>

しかし、メソッドスロットを実行するのは難しい。なぜなら、メソッドスロットはクロージャであり、メソッドそのものではないからである。そのため、メソッドスロットを実行しようとすると、エラーになってしまう。

<?php
require_once "parentobject.php";

$obj = new ParentObject();  // ペアレントを生成
$obj->name = "PHP";         // スロットを定義

// メソッドスロットの定義
$obj->hello = function() use ($obj) {
    echo "Hello, $obj->name.", PHP_EOL;
};

$obj->hello();  // FIXME: このメソッドは実行できない
?>

PHP Fatal error:  Uncaught Error: Call to undefined method ParentObject::hello() in ...(省略)

これを動作させるためには、まずメソッドではなく、プロパティを参照していると認識させる必要がある。そのために()で囲んでプロパティの参照の優先度を上げる。

<?php
require_once "parentobject.php";

$obj = new ParentObject();  // ペアレントを生成
$obj->name = "PHP";         // スロットを定義

// メソッドスロットの定義
$obj->hello = function() use ($obj) {
    echo "Hello, $obj->name.", PHP_EOL;
};

($obj->hello)();    // これでメソッドスロットが実行される
?>

Hello, PHP.

以上からもわかる通り、クロージャの参照よりもメソッドの実行が優先されるようである。
しかし、メソッドと同様の形で実行したい。そのためにはどうすれば良いのか?そう、ここで__call()マジックメソッドの出番である。

<?php
final class ParentObject {
    
    /* 省略 */
    
    public function __call($name, $arguments) {
        if (isset($this->slot[$name]) && is_callable($this->slot[$name])) {
            // メソッドスロットがあった場合、それを実行する
            return $this->slot[$name](...$arguments);
        } else {
            // メソッドスロットがなかった場合、例外を投げる
            throw new Error("Undefined method slot '${name}'");
        }
    }
    
    /* 省略 */
    
}
?>

基本的な実装は__get()と同じだが、スロットの型はcallable型(関数型)でなければならないため、より厳密である。また、スロットの参照部分が関数の適用になっている。

<?php
require_once "parentobject.php";

$obj = new ParentObject();  // ペアレントを生成
$obj->name = "PHP";         // スロットを定義

// メソッドスロットの定義
$obj->hello = function() use ($obj) {
    echo "Hello, $obj->name.", PHP_EOL;
};

$obj->hello();  // メソッドスロットの実行
?>

Hello, PHP.

オブジェクトの複製

次にプロトタイプベースOOPと言えば、何と言っても特徴的なのがオブジェクトの複製である。これももちろん実装する。

そのために、スロットにペアレントのスロットの参照を定義するように(いわゆるプロトタイプチェーンを実装)したい。それを考慮したうえで複製できるようにした関数がcreate_child()メソッドである。

<?php
class ParentObject {
    
    /* 省略 */
    
    public function create_child() {
        // ペアレントのディープコピー
        $child = clone $this;
        // スロットにペアレントスロットの参照を代入
        $child->slot = [
            "parent" => &$this->slot
        ];
        return $child;
    }
}
?>

これでとりあえずは複製が完了した・・・のだが、このままでは$obj->propの形でスロットの参照ができない。

<?php
require_once "parentobject.php";

$obj = new ParentObject();  // ペアレントを生成

/* nameスロット, hello()メソッドスロットの定義は省略 */

$obj2 = $obj->create_child();   // オブジェクトの複製
$obj2->hello();                 // FIXME: メソッドの実行はできない
?>

PHP Fatal error:  Uncaught Error: Undefined method slot 'hello' in ...(省略)

そこで、__get()__call()を以下のように修正する。

<?php
class ParentObject {
    private $slot = [];
    
    public function __get($name) {
        $slot = $this->search_slot($name);
        if ($slot !== null) {
            // スロットがあった場合、それを返す
            return $slot;
        } else {
            // スロットがなかった場合、例外を投げる
            throw new Error("Undefined slot '${name}'");
        }
    }
    
    /* 省略 */
    
    public function __call($name, $arguments) {
        $method = $this->search_slot($name);
        if ($method !== null && is_callable($method)) {
            // メソッドスロットがあった場合、それを実行する
            return $method(...$arguments);
        } else {
            // メソッドスロットがなかった場合、例外を投げる
            throw new Error("Undefined method slot '${name}'");
        }
    }
    
    /* 省略 */
    
    private function search_slot($name) {
        // スロットを参照
        $slot = $this->slot;
        
        // スロットの存在を確認するまで繰り返し
        while (!isset($slot[$name])) {
            if (isset($slot["parent"])) {
                // ペアレントがいればそのスロットを参照
                $slot = $slot["parent"];
            } else {
                // スロットが存在していなければnullを返す
                return null;
            }
        }
        
        // 存在が確認されたスロットを返す
        return $slot[$name];
    }
    
    /* 省略 */
    
}
?>

スロットを探し出すメソッドsearch_slot()を追加し、それを__get()__call()で呼び出すようにした。これで複製してもペアレントのスロットを参照できるようになった。

<?php
require_once "parentobject.php";

$obj = new ParentObject();  // ペアレントを生成

/* nameスロット, hello()メソッドスロットの定義は省略 */

$obj2 = $obj->create_child();   // オブジェクトの複製
$obj2->hello();                 // メソッドスロットの実行
?>

Hello, PHP.

また、__set()でペアレントスロットの参照が書き換えられないように、__isset()でペアレントスロットも存在しているかどうか確認できるようにする。

<?
class ParentObject {
    
    /* 省略 */
    
    public function __set($name, $value) {
        if ($name !== "parent") {
            // スロットの定義を行う
            $this->slot[$name] = $value;
        } else {
            throw new Error("Cannot define a slot '${name}'");
        }
    }
    
    /* 省略 */
    
    public function __isset($name) {
        // スロットがあった場合にtrue、なければfalse
        return $this->search_slot($name) !== null ? true : false;
    }
    
    /* 省略 */
    
}
?>

コンストラクタ関数の適用

別になくても良いとは思うが、あったほうが便利である。create_child()メソッドを改造する。

<?php
class ParentObject {
    
    /* 省略 */
    
    public function create_child($constructor = null, ...$arguments) {
        // ペアレントのディープコピー
        $child = clone $this;
        // スロットにペアレントスロットの参照を代入
        $child->slot = [
            "parent" => &$this->slot
        ];
        if ($constructor !== null) {
            // チャイルドにコンストラクタ関数を適用する
            $constructor($child, ...$arguments);
        }
        
        return $child;
    }
}
?>

実際にコンストラクタ関数を適用し、オブジェクトを複製してみる。

<?php
require_once "parentobject.php";

$obj = new ParentObject();  // ペアレントを生成

// オブジェクトの複製(コンストラクタ付き)
$obj2 = $obj->create_child(function($child, $name) {
    $child->name = $name;
    $child->hello = function() use ($child) {
        echo "Hello, $child->name.", PHP_EOL;
    };
}, "PHP");

$obj2->hello(); // メソッドスロットの実行
?>

Hello, PHP.


最終的なプロトタイプベースOOPの実装は以下のようになる。

<?php
final class ParentObject {
    private $slot = [];
    
    public function __get($name) {
        $slot = $this->search_slot($name);
        if ($slot !== null) {
            // スロットがあった場合、それを返す
            return $slot;
        } else {
            // スロットがなかった場合、例外を投げる
            throw new Error("Undefined slot '${name}'");
        }
    }
    
    public function __set($name, $value) {
        if ($name !== "parent") {
            // スロットの定義を行う
            $this->slot[$name] = $value;
        } else {
            throw new Error("Cannot define a slot '${name}'");
        }
    }
    
    public function __call($name, $arguments) {
        $method = $this->search_slot($name);
        if ($method !== null && is_callable($method)) {
            // メソッドスロットがあった場合、それを実行する
            return $method(...$arguments);
        } else {
            // メソッドスロットがなかった場合、例外を投げる
            throw new Error("Undefined method slot '${name}'");
        }
    }
    
    public function __isset($name) {
        // スロットがあった場合にtrue、なければfalse
        return $this->search_slot($name) !== null ? true : false;
    }
    
    public function __unset($name) {
        // 代わりに$this->slot[$name]を適用
        unset($this->slot[$name]);
    }
    
    private function search_slot($name) {
        // スロットを参照
        $slot = $this->slot;
        
        // スロットの存在を確認するまで繰り返し
        while (!isset($slot[$name])) {
            if (isset($slot["parent"])) {
                // ペアレントがいればそのスロットを参照
                $slot = $slot["parent"];
            } else {
                // スロットが存在していなければnullを返す
                return null;
            }
        }
        
        // 存在が確認されたスロットを返す
        return $slot[$name];
    }
    
    public function create_child($constructor = null, ...$arguments) {
        // ペアレントのディープコピー
        $child = clone $this;
        // スロットにペアレントスロットの参照を代入
        $child->slot = [
            "parent" => &$this->slot
        ];
        if ($constructor !== null) {
            // チャイルドにコンストラクタ関数を適用する
            $constructor($child, ...$arguments);
        }
        
        return $child;
    }
}
?>

*1:Pythonでは難しいので、ここめっちゃ強調したかった