is_xxx() 関数を使わずに型判定できますか?


問題

\CarrotRakko という名前空間の直下に以下の8つの関数を定義せよ。定義する関数のシグネチャと外から見た挙動は、グローバル名前空間に存在する同名の関数と同じにすること。

関数/メソッド呼び出しは行なってはならない。ここで、関数/メソッドの呼び出しとはPHP処理系の実装レベルで関数/メソッドがコールされることを指すのであって、PHPコードでの呼び出し方は問わない。なお、以下のソースコードでは関数/メソッド呼び出しを行わずに実装できる部分を、関数/メソッド呼び出しによって簡潔に表現することがある。

実装に用いる言語およびバージョンは PHP 7.4.x とする。公式でない処理系や標準でバンドルされていないエクステンション、自作のエクステンションの使用などは認めない。

問1: is_bool(mixed $var): bool
問2: is_int(mixed $var): bool
問3: is_float(mixed $var): bool
問4: is_string(mixed $var): bool
問5: is_array(mixed $var): bool
問6: is_object(mixed $var): bool
問7: is_resource(mixed $var): bool
問8: is_null(mixed $var): bool

問1: is_bool(mixed $var): bool

最初に思いついたのは次のような実装です。

<?php

namespace CarrotRakko;

/**
 * boolean型かどうか
 *
 * @param mixed $var
 * @return bool
 */
function is_bool($var): bool
{
    return ($var === true) || ($var === false);
}

boolean型 には truefalse の2値しかないのでこういうことができます。しかしながら、型の中には取りうる値が有限個ではないもの( string型, array型, object型, resource型 )や、有限個ではあるがリテラルをベタがきするには多すぎるもの( int型, float型 )があるため、boolean型 の値の種類の少なさに依存しない実装をしてみたいところです。

function is_bool($var): bool
{
    return !!$var === $var;
}

ここで 論理否定演算子Logical Not Operator) について確認しておきたいと思います。 !$var が評価されるときはまず、 $varboolean型に変換 されます。変換された結果が true であれば !$varfalse に、 false であれば !$vartrue に評価されます。

論理否定演算子の確認が済んだところで、 booleanへの変換Converting to boolean)を理解しましょう。リンク先を読むと、PHPの8つの型はいずれもboolean型に変換できることがわかります。

ここで、 ! を使って実装した is_bool(mixed $var): bool 関数が正しい挙動をすることを証明できるようになりました。 !!$var は常にboolean型なので、 $var がboolean型ではないときには !!$var === $varfalse に評価されます。 $vartrue のときも false のときも !!$var === $vartrue に評価されるので、この関数の挙動が正しいことを証明できました。

!! してから元々の値と比較するというような便利なことは他の型ではできそうにないので、もうちょっと一般化しておきましょう。

function is_bool($var): bool
{
    return (bool)$var === $var;
}

問2: is_int(mixed $var): bool

整数への変換Converting to integer)を読むと、boolean型, int型, float型, string型, resource型, null型の6つの型はint型に変換できるようです。array型, object型の2つの型をint型に変換しようとしたときの挙動は未定義と書いてあります。

循環呼び出しにならないように is_array() , is_object() を定義できると期待すると、次のような実装ができます。

<?php

namespace CarrotRakko;

/**
 * int型かどうか
 *
 * @param mixed $var
 * @return bool
 */
function is_int($var): bool
{
    return !is_array($var)
        && !is_object($var)
        && (int)$var === $var;
}

問3: is_float(mixed $var): bool

float への変換Converting to float)を読むと、float型への変換が定義されている型とint型への変換が定義されている型は同じことがわかります。したがって、 is_int() を実装したときと同じ期待のもとで次のような実装が可能です。float型には NAN という NAN !== NAN な値があることに注意が必要です。

<?php

namespace CarrotRakko;

/**
 * float型かどうか
 *
 * @param mixed $var
 * @return bool
 */
function is_float($var): bool
{
    return !is_array($var)
        && !is_object($var)
        && (
            (float)$var === $var
            || $var !== $var
        );
}

問4: is_string(mixed $var): bool

文字列への変換Converting to string)を読みましょう。array型とobject型があやしいので実験してみます。

(string)[];
// => PHP Notice:  Array to string conversion

(string)(new class() {});
// => PHP Fatal error:  Uncaught Error: Object of class class@anonymous could not be converted to string

諦めて is_int() , is_float() と同様の実装をします。

<?php

namespace CarrotRakko;

/**
 * string型かどうか
 *
 * @param mixed $var
 * @return bool
 */
function is_string($var): bool
{
    return !is_array($var)
        && !is_object($var)
        && (string)$var === $var;
}

問5: is_array(mixed $var): bool

配列への変換Converting to array)を読むと、8つの型はいずれもarray型に変換できるので、次の実装ができます。

<?php

namespace CarrotRakko;

/**
 * array型かどうか
 *
 * @param mixed $var
 * @return bool
 */
function is_array($var): bool
{
    return (array)$var === $var;
}

問6: is_object(mixed $var): bool

オブジェクトへの変換Converting to object)を読むと、8つの型はいずれもobject型に変換できるので、次の実装ができます。

<?php

namespace CarrotRakko;

/**
 * object型かどうか
 *
 * @param mixed $var
 * @return bool
 */
function is_object($var): bool
{
    return (object)$var === $var;
}

問7: is_resource(mixed $var): bool

リソースへの変換Converting to resource)を読むと、resource型に関しては今までと同じ戦略が通用しそうにありません。循環呼び出しにならないように is_null() を定義できる自信があるので、次のような実装でゴリ押します。

<?php

namespace CarrotRakko;

/**
 * resource型かどうか
 *
 * @param mixed $var
 * @return bool
 */
function is_resource($var): bool
{
    return !is_bool($var)
        && !is_int($var)
        && !is_float($var)
        && !is_string($var)
        && !is_array($var)
        && !is_object($var)
        && !is_null($var);
}

問8: is_null(mixed $var): bool

遊ぶ順番を間違えた感...。

<?php

namespace CarrotRakko;

/**
 * null型かどうか
 *
 * @param mixed $var
 * @return bool
 */
function is_null($var): bool
{
    return null === $var;
}

展望

関数を実装してみたはいいものの、エラーを吐かないかどうかや、挙動がビルトインの関数と同じかのテストをする必要があるなと思っています。

また、PHPには8つの型を判定する関数以外にも is_xxx() な名前の関数があるので、挙動を理解して再発明してみたいですね。

問9: is_a
問10: is_callable
問11: is_countable
問12: is_finite
問13: is_infinite
問14: is_iterable
問15: is_nan
問16: is_numeric
問17: is_scalar
問18: is_subclass_of