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
をやめて、クラスを分けてTuple2
、Tuple3
、Tuple4
のようにしても良い。
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クラスがあるとする。
/**
* @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でやるなよという気がしないでもないが。