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

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

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

```php
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`で

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

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

```php
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も作ってみる。

```php
/**
 * @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`をやめて、クラスを分けて`Tuple2`、`Tuple3`、`Tuple4`のようにしても良い。

```php
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のように自分で`Tuple`に`Tuple`を入れる構造にすると増やせるが…。


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



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

```php
/**
 * @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`テーブルがあったとして、それぞれのカラムに対応するクラスを作る。

```php
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のクエリを何とかして良い感じに作る。

```php
/**
 * @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`が出来る。


```php
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でやるなよという気がしないでもないが。