本セッションの登壇者
セッション動画(YouTubeチャンネル登録もお願いします。)
こんにちは。「5分でわかるPHPの型システム」というタイトルで話していきたいと思います。
うさみといいます。ピクシブ株式会社で働いています。今回は5分のLTで、時間がないので巻きで進めます。
PHPは簡単な言語?
PHPって皆さんご存知の通り、「超簡単な言語で上級テクニックなんかないよ」と言いたいところでした。現実にはPHPは言語が簡単すぎるので、簡単に書いてしまうとエントロピーが無限に増大していきます。
PHPのパブリックイメージ
皆さん、PHPの型にどのような印象をお持ちでしょうか? おそらく、いろいろな印象をお持ちでしょう。
だいたい間違っていますが、そのあたりについて話していきましょう。
PHPの型 - 共用体と構造体
PHPの型というのは実装上どうなっているかというと、C言語レベルではこのような共用体と構造体になっています。
「PHPの値は型タグと共用体の組に過ぎませんが何か?」という極論は(過言なので)置いておいて、PHPのランタイムの型はこれだけです。言語処理系の内部実装に興味がある方は、とても良い資料(Rubyソースコード完全解説)がWebにあるのでご参照ください。また、zvalで検索していただくとPHPの内部構造について解説した記事も見付かるはずです。
PHPで使える型
あらためてPHPの持っている型というのは、ほぼほぼこれだけです。
上のに太字で並べているのはPHPの基本的な型で、ほとんどJSONにマッピングできるものです。下のほうはJSONに対応しない特殊な型です。
「PHPはarray指向プログラミング言語」とか僕は言ったりしますが、PHPの特徴としていろいろなところでarrayが出てきます。これはほかの言語でいうlistとかdict、Hashをあわせ持った変な型です。
ユニオン型(A|B)みたいなものがあります。これはAかBのどちらかの型をとります。交叉型(A&B)はAおよびBを取ります。PHPは単一継承の言語なのでどちらも継承するということはできず、基本的にはインターフェースを書くことになります。これらの対照的な構造の型のうち、ユニオン型はしばしば使いたくなると思いますが、実際には交叉型はあまり使う機会はありません。
それからnull許容型です。PHPは基本的に型宣言ができるので、ぬるぽ(NullPointerException)は起きないようになっています。つまり、型宣言された引数やメソッドの戻り値にnullが混入して実行時エラーを起こすことはないということです。nullを許容したいときは型の前に「?」をつけることで許容できます。
ほかにPHP用語としてはmixedという型があります。これはPHPの型システムにおける「トップ型」で、TypeScriptやいろいろな言語でいうところのanyです。
あとはresource、これはファイルポインタなどですね。PHP 8.1になってから肩身が狭くなっていますが、そのような特別な型もあります。
PHPの型は実行時に保障される
PHPの型システムの特徴は、処理系によって実行時に保障されていることです。
このadd()関数の宣言があって、intで型宣言しているので文字列を渡してもきちんと実行時にキャストされます。
このように'a'という整数ではない文字列を渡してやるとTypeErrorが吐かれます。
先ほどの例だと、文字列の'0'を渡してもキャストされたのですが、今回はファイル単位で「declare(strict_types=1);」を設定することで、実行時に自動変換せずTypeErrorを起こすように制御できます。今回の場合では文字列の'0'と'1'が渡されるとTypeErrorが発生します。
どちらにせよ、受け取った側の関数の内部実装ではintであることが保障されます。小さいスコープの中で確実に保障される型になっています。
あとはPHPは実行時に確実に型情報が取れるようになっているので、関数を使ってやれば型判定ができたりします。
それからユニオン型宣言です。今回でいえばintまたはfloat、実行時にどちらの型の値が渡されたのか確実に取得できるようになっています。
classとinterfaceをユーザー定義型のように使う
classとinterfaceを型宣言、ユーザー定義型のように使うことができます。今回であればDateTimeという型を受け取ってプリントすることができたりします。
interfaceを型として使うこともできます。今回でいえばDateTimeとDateTimeImmutableはそれぞれDateTimeInterfaceで継承したクラスなので、インターフェイスで型宣言することによって、この関数はどちらも受け入れられます。
あと、voidは関数が有効な戻り値を戻さないという型宣言で、これがあることで関数には副作用。たとえば何か外部に出力したりグローバルな状態で入出力していることを示せます。これも重要な型宣言です。
また、neverという型宣言もあります。これは関数が絶対に正常に終了しないという型宣言です。関数を呼び出すことで必ずexitでプロセスが終了したり、必ず例外をスローさせたりするものに使います。どちらも起こらず異常なく関数の処理が終了した時にはTypeErrorでスローされるので、正常終了することはないとランタイムに保障された型だと言えます。逆にいうと正常に処理が完了する余地があるものにnever型宣言は使えません。
PHPでつらいこと - 無限の(悪い)可能性を秘めたarray
型が実行時に保障される点で、PHPはほかの動的言語、PythonやRubyとは一線を画しているといえます。「静的型付きプログラミング言語」といってもいいと思います。
実際にはつらいことはいろいろあって、「外部の入出力の部分で型が何なのかわからない、無限に発散してしまう」といったところがあります。
こういうところを扱っていくと、とてもつらい…みたいなことがあったりします。
あとarrayはどんなものでも内包してしまう悪い意味で無限の可能性を持った型なので、これをどうにかしたいです。
array_map()みたいなものもあるのですが、型宣言だけを追っていくと「arrayを受け取ってarrayを返す」という変なものになってしまうので、「型が何もわからない」「mixedやanyがそこかしこに出てくる」といった感じになってきます。
arrayを放置しないためにできること
これを放置して開発が進むと、局所的に型が不明でつらい箇所がプロジェクト中に増殖していきます。ランタイムに保障されないきちんとした型を認識して検査するためのツールがいくつかあります。
PHPではクラスや関数・メソッドに /** … */ の形式でコメントを記述すると、DocBlockという特殊なブロックとして扱われます。これをメタデータとして扱う記法を一般にPHPDocと呼びます。
先述したPHPStanなどのツールは、定義に基づく型推論だけではなく、PHPDocのコメントを読み取ってPHP言語組み込みの型宣言では表現できない細かな型を認識します。残念なことにPHPDocで記述できる型には標準私用がないのですが、ここでは多くの先進的なツールが解釈できるデファクトなものを紹介します。
array-shapes(またはObject-like arrays)はarrayの内部にどういったものがあるか説明する型です。このコードでは、配列内部のキー"name"にはstring型の値が、"age"にはint型の値が格納されることを指定しています。
あとはarrayとlistです。PHPのarrayは、Pythonのdictとlistの性質を併せ持った型です。単にstring[]やarray<string>と記述すると、dictのように文字列をキーにした辞書なのか、listのように値が線形に並んだものなのかを区別できません。
list<string>のように書くと、ほかの言語の配列のように「値が線形に並んだものである」という宣言ができます。
また、@templateタグで型パラメータを宣言することでジェネリクスを使うこともできます。これによって入力値やクラスで保持している型パラメータに応じて関数やメソッドの戻り値を決定させることができます。
@paramはパラメータを、@returnは戻り値を記述するタグですが、PHPDocを解釈する古いツールの一部はジェネリクスを使った型を認識できないことがあります。このようなものは@paramに代えて@phpstan-paramや@phpstan-returnと記述することで互換性を確保できます。
今回紹介したPHPStan、Psalm、PhpStormの3種類のツールはすべて@phpstan-プレフィクスの有無に拘らず型を認識できます。
型は動的言語のプログラムが持つ無限の可能性を人間が把握できる領域まで落とし込めるテクニックです。型を付けることでアプリケーションのエントロピーを減少させて、楽しく開発していきましょうというお話でした。
以上です。
話させていただきました! 補足もしてあるので文章だけ読んでもらえればよいかと思います