型宣言を利用した自動的プログラミング(という夢)

Posted by Hiraku on 2016-12-02

PHPアドベントカレンダー2016の2日目です。この記事に合わせてライブラリでも作ろうと思ってたのですが、全然間に合わなかったので夢という体でポエムを書こうと思います。

祝7.1リリース

ちょうど記事を書いていたらPHP7.1がリリースされていました。おめでとうございます!みんな今すぐビルドするんだ!

http://php.net/archive/2016.php#id2016-12-01-3

去年も12月1日に7.0がリリースされてたので、毎年JSTだと12月2日に新しいPHPが手に入るような感じですね。

今回はマイナーバージョンアップなのでそんなに劇的な機能はありませんが、個人的にはiterable型が超欲しかったやつです。

  • Nullable types
  • Void return type
  • Iterable pseudo-type
  • Class constant visiblity modifiers
  • Square bracket syntax for list() and the ability to specify keys in list()
  • Catching multiple exceptions types
  • Many more features and changes…

PHP7からできるようになったことのおさらい

さて、そもそもPHP7の記法を有効活用する議論は今までそんなにされてなかったと思います。

PHP7での目玉機能といえば、どちらかと言うと「速くなりました」が目立っていて、型宣言に対する扱いはコミュニティ内でも扱いが定まっていません(私の肌感)。

  • 速くなりました
  • スカラ型を型宣言できるようになりました
  • 戻り値も型宣言できるようになりました
  • AST構築
  • 名前空間をまとめてuseできたり
  • 宇宙演算子とか
  • etc

レガシーコードと戦って綺麗にした話もいいのですが、たまには未来っぽい話をPHPでも聞きたいものです。 というわけで、「型宣言」に関するポエムが今回のテーマです。

タイプヒンティング改め型宣言

PHP7の関数・メソッドには、どんな値を受け取るか、そしてどんな値を返すかという情報を記述することができます。

<?php
function add(int $a, int $b): int
{
    return $a + $b;
}

まあ、ジェネリクスがないので「型の一部分を引数化したい」なんてことはできませんし、関数やジェネレーターに関してもcallableGeneratorでザクっとしか記述できません。

それでも一応、一通り宣言を書いていくことができるようになったということなのか、公式ドキュメントでも「タイプヒンティング」ではなく「型宣言(Type declarations)」という風に名称が改められています。

型検査の微妙感

しかし、PHPは動的型付き言語です。つまり型宣言をいくら綿密にやろうと、言語公式には実行時の動的な検査までしかできません。「この辺は型制約に違反してるから、バグだよ」を全パス調べ上げることなんてできないです。

もし3rdPartyのツールが頑張って検査してくれたとしても、PHPの言語本体に、そういった静的検査を壊すような機構がいっぱい含まれています。クラスの文字列をnewできたり、可変変数やcompactやextractのようなシンボルテーブルを直接いじれるものもあります。

それに、動的検査にしても、子クラスで上書きできることはいっぱいあります。インターフェースで縛ってでも居ない限りメソッドのプロトタイプは変更できますし、アップキャストのような概念もないので存在しないメソッドだろうと呼びたい放題です。

だいたい、世の中の静的型付き言語は型推論を頑張る方向で進化していて、型宣言は補助的な範囲に留められるようになっています。

PHPは何がしたいのでしょう? 動的片付けでそもそも宣言なんて要らないくせに、宣言できるようにするなんて。

モダンな言語ならもっと短い記述で、しかももっと強力な検査ができるのに対し、PHPはやたら冗長な構文を欠かされ、それでもショボい検査しかできないわけです。

何でしょうねこの徒労感

いや、まあちゃんと型宣言していけば、動的検査してくれるだけ嬉しくはありますが、ちょっとオシャレにassert()を書いてるのと変わりません。見返りがその程度なのに、ちまちま型宣言したいですか?

型宣言を逆に利用する試み

ここまでが私の認識で、じゃあその上でどうしたら面白いことができるかを考えていました。

その昔、私は「PHPのインターフェースは、クラスに付与するただのメタ情報である」みたいな主張をしたことがあります。

PHPのinterfaceとは何か: Architect Note

型宣言に関しても、似たようなものだと考えられます。

  • 型宣言 = このコードはこういう意味であるという付加情報

こんなふうに考えると、ぱっと思いつくのがDIコンテナでの活用方法です。

PHPとDI (Dependency Injection)

なんで動的な言語であるPHPでDIの話題が出てくるかというと、動的言語のくせに関数やクラスの再定義を(言語標準では)許可していないからです。

イマドキのソフトウェアは小さくテスト可能な単位(PHPだとクラス)で細かく作り、組み立てるのが普通です。 「小さくテスト可能な」を突き詰めていくと、こんなクラスが出来上がります。

  • コンストラクタ引数やメソッドの引数で与えられたインスタンスだけ使って処理を行う
  • newしない
    • 例外(Exception)やValueObject, Entityの類は問題ないと思う(流派あり)
  • クラス名を型宣言とinstanceof以外では記述しない
  • グローバルを参照しない。「現在時刻」「環境変数」「グローバル定数」といったものも参照しない。
  • グローバルに書き込まない。echoやerror_log、file_put_contentsなども直接は実行せず、何らかの抽象インスタンスへのメソッド呼び出しに置き換えておく

これらを守ったクラスのテストは非常に簡単に書けるようになります。「特定の引数を与えたら」「特定の戻り値が返る / 与えられたオブジェクトに対しメッセージを送る」これだけしかしてないわけですから。 長大なDBセットアップ、ミドルウェアを立てたりしなくても、そのクラス単体に関するテストは書けるはずですよね。

しかし、いつかどこかでグローバルと繋いだり、newをしなければ、アプリケーションとして役に立つものは完成しないわけです。気持ち悪い部分を固めて先送りになっているけれど、結局は対応しなければいけません。

で、そのテストしづらい気持ち悪い部分を多少エレガントにするために、DIコンテナやサービスロケータといったライブラリを使います。

原始的なDIの設定

原始的なDIコンテナは、ただのクラスで表現することができます。

<?php
// container.php とします
return new class {
    public function getNow() {
        return new DateTimeImmutable();
    }
    
    public function getService1() {
        return new HogeService($this->getNow());
    }
    
    public function getService2() {
        return new FugaService($this->getNow());
    }
    
    //...
    //...
};
<?php
// phpの起動スクリプトだとします
require 'vendor/autoload.php';

$container = requrie 'container.php';
$container->getService2()->run();

newしていたり、グローバルを参照している汚い箇所はcontainer.phpに押し込めていきます。あるクラスをnewしている部分を$this->を使い、コード中で一箇所に固めているのがポイントです。

無名関数とハッシュを使っていたりYAMLで書けたりと、他にも色々な記法のDIコンテナが世の中には存在します。

auto wiring

で、ふと思うわけです。「なにこれ面倒くさい」と。

もうね、クラスを綺麗に分割するというのは大変な作業です。確かにテストは書きやすいかもしれませんが、こんな風に手作業で組み立て処理を書いていると、コード量も増えますし、ぶっちゃけテスト以外ではクラスを変更したいときなんて大してありませんし、「あたし、一体何してんのかな―」と疲れます。

ソースコードの分割と組立。

分割する方は、人間がやるしかないでしょう。ドメインロジックがどういうものなのか理解していないと書けません。 しかし組立は?組立はある程度自動化できるのではないでしょうか。

ということで、最近のDIコンテナは多かれ少なかれ自動で組み立てる仕組みが入っています。

すごく素朴なところだと、コンストラクタ引数の型を見て、それをインスタンス化して進めてしまう、という感じですね。

<?php
class Hoge {
    function __construct(DateTime $now) {
        //...
    }
}

//...

// このHogeクラスをDIコンテナのauto wiringに解決させると、
// new Hoge(new DateTime) したのと同じインスタンスが得られる

auto wiringを支えそうな型宣言

SymfonyのDI Componentは仮引数名なども駆使してかなり強力に組立を行ってくれるようです。 しかし最新のPHPならもっとできることってあるような気がします。勝手に妄想を書いてみます。

[案1]マーカーインターフェースによる戦略の変更

<?php
interface SingletonMarker {}

例えば組み込みのインターフェースとしてこんなのを用意しておいて、「このインターフェースを実装しているクラスは、一度しかインスタンス化しない(インスタンス化したらキャッシュして常に使いまわす)」みたいな意味を与えます。

<?php
class Logger implements Psr\Log\LoggerInterface, SingletonMarker
{
    // ...
}

なにもプロトタイプを持たない、いわゆるマーカーインターフェースは、どんなクラスにも必ず混ぜ込むことができます。implementsをちょっと書くだけでシングルトン化し、手軽に高速化が可能です。

[案2]トレイトによるインジェクション

リフレクションを使うと、あるクラスがuseしているトレイト一覧を取得できます。これを使って、「あるトレイトをuseしていたら、そのトレイトのsetterメソッドを自動で呼び出す」なんていう自動化ができそうです。

例えばこんな感じのトレイトを作って、

<?php
trait LoggerAwareTrait
{
    private $logger;
    public function setLogger(Psr\Log\LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
}

DIコンテナに「インジェクション用ですよ」と伝えておくと、あとはuseしているクラスを作る際、DIコンテナが勝手に発動します。

<?php
class MyController
{
    use LoggerAwareTrait;
    
    public function hogeAction()
    {
        // LoggerAwareTraitによって、$this->loggerは勝手に使えるようになっている
        $this->logger->notice('ヤバイ');
    }
}

コンストラクタインジェクションだと、コンストラクタを自分で書き換える必要があり面倒さが残ります。トレイトの場合はuseするだけで、汎用的に使いまわすオブジェクトを撒くことができます。 トレイトには強制力がありませんのでコケる可能性もありますが、こんなことができたら十分便利でしょう。

[案3]Configや環境変数を個別にバラまく

ところでスカラ型を型宣言に使えるようになったことで、アプリケーションの設定値を撒きやすくなりました。 今までは設定値と言えば「Config」みたいな名前のクラスを作って、丸ごとアプリケーション内に投げ渡しているケースが多かったと思います。

しかし大抵の場合、必要なのはConfigの中の一部の値だけです。

  • 型は string
  • 仮引数名が特定の文字列

これだけ揃っていれば、auto wiringで直接定数を投げ込んでいくことも可能そうです。

<?php
class Hoge
{
    public function __construct(string $DB_USERNAME, string $DB_PASSWORD)
    {
         //...
    }
}

Configクラスに依存しなくなり、より単純なクラスになっていきます。

型宣言を検査ではなく、実装を作るために使う

DIコンテナでは、型宣言を活用してはいますが、使い方が型検査と真逆です。

むしろ、例えばコンストラクタ引数を DateTimeImmutable $now から DateTime $now に変更したとしたら、auto wiringが勝手に組み換え、 DateTime $now を渡すように動的にプログラムを変えていってしまいます。この世界観では実行時検査なんて通るに決まっています。

auto wiring機構が新たなインスタンスを解決できないときだけ、例外が発生してプログラムがクラッシュします。

こっちのほうが、PHPらしい作り方だと思います。

実装同士の結びつきを遅らせてインターフェイスとプログラミングをする。小さくて確実に動くパーツを作り、最低限の制約だけ書いておいて、細かい組立は自動化する。そういったスタイルであれば、型宣言をペタペタ書くのも悪くないかなと思います。

意味は好きに決めよう

名前空間の意味に関しても、型宣言に関しても、言語側ではさして規定をせず、ユーザー側で好き勝手使えるのがPHPの良いところでもあり、悪いところでもあります。

私も名前空間やトレイトをかなり乱用した、DIコンテナのオレオレ実装を作っているところです。(全然完成してないけど)

まあ、面白い使い方ないかなと日頃から考え、オレオレフレームワークを作るのも悪くないですよ。

PHP7.1で追加されたnullableやvoidなど、また解釈できるメタデータが増えたので、これをどう使うと面白いか妄想するのが次の課題です。

PHPの最新記事