1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

複数の型を動的に変更しつつ静的型チェックしたい

Last updated at Posted at 2020-07-25

arrayの型が全て保存される版のようなものが欲しい。
欲を言わなければ、ListかTupleでもいい。

ということで作ってみる。

class Unit
{
    /** @var Unit */
    public static $unit;
}
Unit::$unit = new Unit;

/**
 * @template V
 * @template N
 */
class TypedList
{
    /** @var V */
    private $value;
    /** @var N */
    private $next;

    /**
     * @param V $value
     * @param N $next
     */
    private function __construct($value, $next)
    {
        $this->value = $value;
        $this->next = $next;
    }


    /**
     * @return V
     */
    public function value()
    {
        return $this->value;
    }

    /**
     * @return N
     */
    public function next()
    {
        return $this->next;
    }
    

    /**
     * @template V1
     * @param V1 $v1
     * @return TypedList<V1,Unit>
     */
    public static function list1($v1)
    {
        return new TypedList($v1, Unit::$unit);
    }


    /**
     * @template V1
     * @template V2
     * @param V1 $v1
     * @param V2 $v2
     * @return TypedList<V1,TypedList<V2,Unit>>
     */
    public static function list2($v1, $v2)
    {
        return new TypedList($v1, new TypedList($v2, Unit::$unit));
    }

    /**
     * @template V1
     * @template V2
     * @template V3
     * @param V1 $v1
     * @param V2 $v2
     * @param V3 $v3
     * @return TypedList<V1,TypedList<V2,TypedList<V3,Unit>>>
     */
    public static function list3($v1, $v2, $v3)
    {
        return new TypedList($v1, new TypedList($v2, new TypedList($v3, Unit::$unit)));
    }

    /**
     * @template N1
     * @param N1 $n1
     * @return TypedList<N1,TypedList<V,N>>
     */
    public function unshift($n1)
    {
        /** @var TypedList<N1,TypedList<V,N>> */
        $ret = new TypedList($n1, $this);
        return $ret;
    }

    /**
     * @template V1
     * @template V2
     * @template V3
     * @param V1 $v1
     * @param V2 $v2
     * @param V3 $v3
     * @return TypedList<V1,TypedList<V2,TypedList<V3,Unit>>>
     */
    public static function list3_2($v1, $v2, $v3)
    {
        $list2 = self::list2($v2, $v3);
        return $list2->unshift($v1);
    }
}

Haskellの data List a = Nil | Cons a(List a)みたいな感じ。
TypedList::list3() で一気に生成しても良いし、 unshift

    $list = $list
          ->unshift(3)
          ->unshift(4)
          ->unshift(new DateTime())
          ->unshift(6)
          ->unshift(7)
          ->unshift(new DateTime());

のように追加しても良い。

function testList(): void
{
    $list3 = TypedList::list3(1, new DateTime(), "foo");

    echo $list3->value();
    echo $list3->next()->value();
    echo $list3->next()->next()->value();
    echo $list3->next()->next()->next()->value();

    $list3_2 = TypedList::list3_2(1, new DateTime(), "foo");
    echo $list3_2->value();
    echo $list3_2->next()->value();
    echo $list3_2->next()->next()->value();

    $list = TypedList::list2(new DateTime(),2);
    $list = $list
          ->unshift(3)
          ->unshift(4)
          ->unshift(new DateTime())
          ->unshift(6)
          ->unshift(7)
          ->unshift(new DateTime());
    echo $list->next()->next()->value();
    echo $list->next()->next()->next()->value();
    echo $list->next()->next()->next()->next()->next()->next()->value();
    echo $list->next()->next()->next()->next()->next()->next()->value()->format('Y-m-d');
    $undef = $list->next()->next()->next()->next()->next()->next()->next()->next()->next();
}
 ------ ---------------------------------------------------------------- 
  Line   functions.php                                                   
 ------ ---------------------------------------------------------------- 
  152    Parameter #1 (DateTime) of echo cannot be converted to string.  
  154    Call to an undefined method Unit::value().                      
  158    Parameter #1 (DateTime) of echo cannot be converted to string.  
  170    Parameter #1 (DateTime) of echo cannot be converted to string.  
  171    Parameter #1 (DateTime) of echo cannot be converted to string.  
  173    Call to an undefined method Unit::next().                       
 ------ ---------------------------------------------------------------- 

phpstanでは期待通りエラーが出る。
ただし見栄えが良くない。処理速度も良くないし100件とか追加したくない。
(異なる型を追加するためのものなので、foreach で使ったりはしないが)

Tupleも作ってみる。

/**
 * @template T1
 * @template T2
 * @template T3
 * @template T4
 * @template T5
 */
class Tuple
{
    /** @var T1 */
    public $v1;
    /** @var T2 */
    public $v2;
    /** @var T3 */
    public $v3;
    /** @var T4 */
    public $v4;
    /** @var T5 */
    public $v5;

    /**
     * @param T1 $v1
     * @param T2 $v2
     * @param T3 $v3
     * @param T4 $v4
     * @param T5 $v5
     */
    public function __construct(
        $v1, $v2, $v3, $v4, $v5
    ){
        $this->v1 = $v1;
        $this->v2 = $v2;
        $this->v3 = $v3;
        $this->v4 = $v4;
        $this->v5 = $v5;
    }

    /**
     * @template V1
     * @template V2
     * @param V1 $v1
     * @param V2 $v2
     * @return Tuple<V1,V2,Unit,Unit,Unit>
     */
    public static function tuple2($v1, $v2)
    {
        return new Tuple($v1, $v2, Unit::$unit, Unit::$unit, Unit::$unit);
    }

    /**
     * @template V1
     * @template V2
     * @template V3
     * @param V1 $v1
     * @param V2 $v2
     * @param V3 $v3
     * @return Tuple<V1,V2,V3,Unit,Unit>
     */
    public static function tuple3($v1, $v2, $v3)
    {
        return new Tuple($v1, $v2, $v3, Unit::$unit, Unit::$unit);
    }

    /**
     * @template V1
     * @template V2
     * @template V3
     * @template V4
     * @param V1 $v1
     * @param V2 $v2
     * @param V3 $v3
     * @param V4 $v4
     * @return Tuple<V1,V2,V3,V4,Unit>
     */
    public static function tuple4($v1, $v2, $v3, $v4)
    {
        return new Tuple($v1, $v2, $v3, $v4, Unit::$unit);
    }

    /**
     * @template V1
     * @template V2
     * @template V3
     * @template V4
     * @template V5
     * @param V1 $v1
     * @param V2 $v2
     * @param V3 $v3
     * @param V4 $v4
     * @param V5 $v5
     * @return Tuple<V1,V2,V3,V4,V5>
     */
    public static function tuple5($v1, $v2, $v3, $v4, $v5)
    {
        return new Tuple($v1, $v2, $v3, $v4, $v5);
    }

    /**
     * @template V3
     * @param V3 $v3
     * @return Tuple<T1,T2,V3,T4,T5>
     */
    public function set3($v3)
    {
        return new Tuple($this->v1, $this->v2, $v3, $this->v4, $this->v5);
    }

    /**
     * @template V1
     * @template V2
     * @template V3
     * @param V1 $v1
     * @param V2 $v2
     * @param V3 $v3
     * @return Tuple<V1,V2,V3,Unit,Unit>
     */
    public static function tuple3_2($v1, $v2, $v3)
    {
        $tuple2 = self::tuple2($v1, $v2);
        return $tuple2->set3($v3);
    }
}

tuple3のように一気に生成しても良いし、tuple3_2のように入れ替えても良い。
Unitをやめて、クラスを分けてTuple2Tuple3Tuple4のようにしても良い。

function testTuple(): void
{
    $t3 = Tuple::tuple3(1, "aaa", new DateTime());
    echo $t3->v1;
    echo $t3->v2;
    echo $t3->v3;
    echo $t3->v4;

    $t3_2 = Tuple::tuple3_2(1, "aaa", new DateTime());
    echo $t3_2->v1;
    echo $t3_2->v2;
    echo $t3_2->v3;
    echo $t3_2->v4;


    $t5 = Tuple::tuple5(1, "aaa", new DateTime(), [1 => "a"], true);
    echo $t5->v1;
    echo $t5->v2;
    echo $t5->v3;
    echo $t5->v4[1];
    echo $t5->v5;

    $t5 = Tuple::tuple5(1,null,3,null,5);
    echo number_format($t5->v1);
    echo number_format($t5->v2);
}
 ------ --------------------------------------------------------------------------- 
  Line   functions.php                                                              
 ------ --------------------------------------------------------------------------- 
  305    Parameter #1 (DateTime) of echo cannot be converted to string.             
  306    Parameter #1 (Unit) of echo cannot be converted to string.                 
  311    Parameter #1 (DateTime) of echo cannot be converted to string.             
  312    Parameter #1 (Unit) of echo cannot be converted to string.                 
  318    Parameter #1 (DateTime) of echo cannot be converted to string.             
  324    Parameter #1 $number of function number_format expects float, null given.  
 ------ --------------------------------------------------------------------------- 

こちらの方が単純で良いかもしれない。でもTypedListのように無限に追加できたりはしない。
btreeのように自分でTupleTupleを入れる構造にすると増やせるが…。

アプリケーションプログラムで頑張らずにPHPStanのDynamicMethodReturnTypeExtensionとの合わせ技でやるべきかもしれないな。

結局何がやりたいかというと、このような良い感じのTupleクラスがあったとすると、同様に変換用のTupleを作る。
template 5個はしんどいのでとりあえず3個のTupleクラスがあるとする。

/**
 * @template T1
 * @template T2
 * @template T3
 * @mixin T1
 * @mixin T2
 * @mixin T3
 */
class DelegateTuple
{
    /** @var T1 */
    public $v1;
    /** @var T2 */
    public $v2;
    /** @var T3 */
    public $v3;

    /**
     * @param T1 $v1
     * @param T2 $v2
     * @param T3 $v3
     */
    public function __construct($v1, $v2, $v3)
    {
        $this->v1 = $v1;
        $this->v2 = $v2;
        $this->v3 = $v3;
    }

    /**
     * @param string $name
     * @param mixed $args
     * @return mixed
     */
    public function __call($name, $args)
    {
        foreach ([$this->v1, $this->v2, $this->v3] as $prop){
            if (method_exists($prop, $name)){
                return $prop->$name(...$args);
            }
        }
    }
}

/**
 * @template C1
 * @template C2
 * @template C3
 * @extends DelegateTuple<class-string<C1>,class-string<C2>,class-string<C3>>
 */
class ColsTuple extends DelegateTuple
{
    /** 
     * @template U1
     * @param class-string<U1> $v1
     * @return self<U1,Unit,Unit> 
     */
    public static function tuple1($v1)
    {
        return new self($v1, Unit::class, Unit::class);
    }

    /** 
     * @template U1
     * @template U2
     * @template U3
     * @param class-string<U1> $v1
     * @param class-string<U2> $v2
     * @return self<U1,Unit,Unit> 
     */
    public static function tuple2($v1, $v2)
    {
        return new self($v1, $v2, Unit::class);
    }

    /** 
     * @template U1
     * @template U2
     * @template U3
     * @param class-string<U1> $v1
     * @param class-string<U2> $v2
     * @param class-string<U3> $v3
     * @return self<U1,U2,U3> 
     */
    public static function tuple3($v1, $v2, $v3)
    {
        return new self($v1, $v2, $v3);
    }

    /**
     * @return DelegateTuple<C1,C2,C3>
     */
    public function convert()
    {
        $ret = new DelegateTuple(
            new $this->v1, new $this->v2, new $this->v3
        );
        return $ret;
    }
}

それから、例えばDBにstaffテーブルがあったとして、それぞれのカラムに対応するクラスを作る。

class staff_staff_id
{
    /** @var int */
    private $staff_id;

    /** @param int $v */
    public function __construct($v)
    {
        $this->staff_id = $v;
    }

    public function staff_id(): int
    {
        return $this->staff_id;
    }
}

class staff_staff_name
{
    /** @var string */
    private $staff_name;

    /** @param string $v */
    public function __construct($v)
    {
        $this->staff_name = $v;
    }

    public function staff_name(): string
    {
        return $this->staff_name;
    }
}
class staff_created_at
{
    /** @var DateTimeInterface */
    private $created_at;

    /** @param string|DateTimeInterface $v */
    public function __construct($v)
    {
        if (is_string($v))
            $v = new DateTimeImmutable($v);
        $this->created_at = $v;
    }

    public function created_at(): DateTimeInterface
    {
        return $this->created_at;
    }
}

それから、DBのクエリを何とかして良い感じに作る。

/**
 * @template T
 * @mixin T
 */
class Result
{
    /** @var T */
    private $cols;

    /** @param T $cols */
    public function __construct($cols)
    {
        $this->cols = $cols;
    }

    /**
     * @param string $name
     * @param mixed $args
     * @return mixed
     */
    public function __call($name, $args)
    {
        return $this->cols->$name(...$args);
    }
}

/**
 * @template T1
 * @template T2
 * @template T3
 * @param ColsTuple<T1,T2,T3> $cols
 * @return array<int,Result<DelegateTuple<T1,T2,T3>>>
 */
function selectStaff(ColsTuple $cols)
{
    $rows = [];

    // db query...

    $res = new Result($cols->convert());
    // set data ...

    $rows[] = $res;
    return $rows;
}

ここまで来て、やっと柔軟なselectが出来る。

function testDelegateTuple(): void
{
    $cols = ColsTuple::tuple3(
        staff_staff_id::class
        , staff_staff_name::class
        , staff_created_at::class
    );

    $rows = selectStaff($cols);

    foreach ($rows as $row){
        echo $row->staff_id();
        echo $row->staff_name();
        echo $row->created_at();
        echo $row->created_at()->format('Y-m-d');
    }


    $cols = ColsTuple::tuple1(
        staff_staff_id::class
    );

    $rows = selectStaff($cols);

    foreach ($rows as $row){
        echo $row->staff_id();
        echo $row->staff_name();
        echo $row->created_at()->format('Y-m-d');
    }
}
 ------ ---------------------------------------------------------------------------------------------- 
  Line   functions.php                                                                                 
 ------ ---------------------------------------------------------------------------------------------- 
  257    Parameter #1 (DateTimeInterface) of echo cannot be converted to string.                       
  270    Call to an undefined method Result<DelegateTuple<staff_staff_id, Unit, Unit>>::staff_name().  
  271    Call to an undefined method Result<DelegateTuple<staff_staff_id, Unit, Unit>>::created_at().  
 ------ ---------------------------------------------------------------------------------------------- 

selectStaffで指定した引数に対応したResultが返ってくる。$row のメソッドが良い感じに自動設定されているのが分かるだろうか。

最初のecho $row->created_at() は型が合わないのでエラー。
二回目のecho $row->staff_name()created_at()はselectしていないのでエラー。

かなり夢が膨らみそう。
Doctrineとか全然見てないけど、こういう柔軟に型が変更できるルールはもう誰か作ってるかな?

PHPStanのGenerics実装のさじ加減ひとつでバグるので、PHPでやるなよという気がしないでもないが。

1
0
3

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?