0
2

More than 3 years have passed since last update.

[PHP]引数で受け取った無名関数(クロージャ)の結果の型を指定する方法

Posted at

引数で受け取った無名関数の返り値が型チェックでエラーになるようにするというのが目的。

お題

phpの引数は型指定できますが、受け取った無名関数の返り値の指定はできません。
例えば下記は配列のフィルタを行うクラスの実装例です。

<?php

class ArrayObj
{
    private $values = [1, 2, 3, 4];

    public function filter(callable $filter): array
    {
        $filteredValue = [];
        foreach ($this->values as $key => $value) {
            if ($filter($value)) {
                $filteredValue[] = $value;
            }
        }
        return $filteredValue;
    }
}

$arrayObj = new ArrayObj();

// array(3) { [0]=> int(1) [1]=> int(3) [2]=> int(4) }
var_dump($arrayObj->filter(function($value) {
    return $value <> 2;
}));

// array(4) { [0]=> int(1) [1]=> int(2) [2]=> int(3) [3]=> int(4) }
var_dump($arrayObj->filter(function($value) {
    return "test";
}));

ArrayObjクラスのfilterメソッドはフィルタリングの判定用の無名関数を受け取ります。bool型を返す無名関数がほしいところですが、文字列を返す無名関数も受け取り処理できてしいます。これをbool型以外を返す無名関数が渡されたときはエラーにするようにします。

型チェック関数を利用する

単純に実装するには型チェック関数を利用するという方法があります。bool型をチェックするので`is_bool関数を使用します。

<?php

class ArrayObj
{
    private $values = [1, 2, 3, 4];

    public function getValues()
    {
        return $this->values;
    }

    public function filter(callable $filter): array
    {
        $filteredValue = [];
        foreach ($this->values as $value) {
            // 型チェック関数を追加
            if (! is_bool($filter($value))) {
                throw new \TypeError();
            }
            if ($filter($value)) {
                $filteredValue[] = $value;
            }
        }
        return $filteredValue;
    }
}

$arrayObj = new ArrayObj();

// array(3) { [0]=> int(1) [1]=> int(3) [2]=> int(4) }
var_dump($arrayObj->filter(function($value) {
    return $value <> 2;
}));

// Fatal error: Uncaught TypeError
var_dump($arrayObj->filter(function($value) {
    return "test";
}));

文字列を返す無名関数を渡すとエラーになりました。しかし

  • 型チェックがバリバリロジックに組み込まれてる
  • エラーハンドリングを自前で実装しなきゃいけない

ていう問題があります。この辺はPHPの型チェックに任せたいところ。

引数の型指定をインターフェースにする

引数の型指定を無名関数ではなく、インターフェースにしてしまうという方法。Javaでいう関数型インターフェースのような使い方。

<?php

// 厳格な型チェックを追加
declare(strict_types=1);

// フィルタリング用のインターフェース定義
interface FilterInterface
{
    function filter($value): bool;
}

class ArrayObj
{
    private $values = [1, 2, 3, 4];

    public function getValues()
    {
        return $this->values;
    }

    // フィルタリング用のインターフェースを実装したクラスを受け取るように変更
    public function filter(FilterInterface $filter): array
    {
        $filteredValue = [];
        foreach ($this->values as $value) {
            // フィルタリング用のクラスでフィルタリング
            if ($filter->filter($value)) {
                $filteredValue[] = $value;
            }
        }
        return $filteredValue;
    }
}

$arrayObj = new ArrayObj();

// array(3) { [0]=> int(1) [1]=> int(3) [2]=> int(4) }
var_dump($arrayObj->filter(new class implements FilterInterface {
    public function filter($value): bool
    {
        return $value <> 2;
    }
}));

// Fatal error:  Uncaught TypeError: Return value of class@anonymous::filter() must be of the type bool
var_dump($arrayObj->filter(new class implements FilterInterface {
    public function filter($value): bool
    {
        return "test";
    }
}));

一応、型チェックをロジックと自前のエラーハンドリングを実装せずにすみました。しかし、利用側がいちいちインターフェースを実装したクラスを実装しなければならないので面倒臭いです。Javaのラムダ式のように省略して記述できればいいのですが、、、

っていうかこれ「無名関数の返り値が型チェックでエラーになるようにする」っていう趣旨からずれまくっているような気がする。

戻り値の型宣言をした無名関数を通す

無名関数の受け取り側で、返り値の型を指定した無名関数に受け取った無名関数を実行させて、返り値の型チェックを行うという方法。ちょっと言ってることがよくわからないですが、具体的にはこんな感じ。

<?php

// 厳格な型チェックを追加
declare(strict_types=1);

class ArrayObj
{
    private $values = [1, 2, 3, 4];

    public function getValues()
    {
        return $this->values;
    }

    public function filter(callable $filter): array
    {
        // 戻り値の型を指定した無名関数
        $test = function (callable $filter, $value): bool {
            return $filter($value);
        };
        $filteredValue = [];
        foreach ($this->values as $key => $value) {
            // 戻り値の型を指定した無名関数で引数の無名関数を実行
            if ($test($filter, $value)) {
                $filteredValue[] = $value;
            }
        }
        return $filteredValue;
    }
}

$arrayObj = new ArrayObj();

// array(3) { [0]=> int(1) [1]=> int(3) [2]=> int(4) }
var_dump($arrayObj->filter(function($value) {
    return $value <> 2;
}));

// PHP Fatal error:  Uncaught TypeError: Return value of class@anonymous::filter() must be of the type bool, string returned
var_dump($arrayObj->filter(function($value) {
    return "test";
}));

これで一応利用側にも負担をかけずに実装できました。まあ、これが現実的なラインかなぁと。

一応、戻り値の型宣言ができるようになったとは言えども、関数の結果を見て判断されるので、一回は実行されてしまいますのでそのへんは注意が必要。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2