Closed tslettebo closed 4 years ago
Using polymorphism, I found one possible solution, adding a "factory method" in the subclass:
<?hh // strict
f();
function f(): void
{
$user=new User();
$user_array=$user->get_item_list();
print_r($user_array);
}
abstract class ActiveRecord<T>
{
public function get_item_list(): array<T>
{
return [$this->make()]; // Result of database query
}
protected abstract function make(): T;
}
class User extends ActiveRecord<User>
{
protected function make(): User
{
return new User();
}
}
Factories are one way to do this. However, I think this is better suited for an upcoming feature, type constants. (We're still working through some details, so haven't really talked much about it yet, but there will eventually be docs, a blog post, etc etc.) The idea is that ActiveRecord
isn't really generic in the way that, say, Vector
is. For the latter, the type parameter is fixed by the calling code, and so can be anything. For the former, the type parameter is fixed by the subclass, and will always be the same for a given subclass. This means we can encapsulate it better, and furthermore might actually be able to have runtime support for new
on it (not implemented yet).
Syntax is something like this. (Warning, uses experimental and partially-implemented features, typed directly into my browser, syntax and behavior very very much subject to change, assuming I remembered it correctly in the first place!!)
<?hh // strict
abstract class ActiveRecord {
abstract type const Record as ActiveRecord;
public function get_item_list(): ImmVector<Record> {
// Since Record is fixed by the subclass, we can do a similar sort of LSB
// lookup as "new static" would do, and so you can in principle do this
// (though it's not implemented yet):
return ImmVector { new Record(), new Record(), new Record() };
}
// ...
}
class User extends ActiveRecord {
type const Record = User;
// ...
}
Thanks for your reply regarding this upcoming feature.
Fortunately, using the current version of Hack and final
classes, the factory method can be identical in all subclasses of ActiveRecord:
final class User extends ActiveRecord<User>
{
<<__Override>> protected function make(): this
{
return new self();
}
}
Since $this
in the base class actually has the right type (User
in this case), I figured maybe this could work, obviating the need for a factory method or type constant, alas, it doesn't work:
abstract class ActiveRecord<T>
{
public function get_item_list(): array<T>
{
invariant($this instanceof T,"");
return [clone $this]; // Result of database query
}
}
The invariant
gives this error message: "Generics can only be used in type hints since they are erased at runtime."
Yet, if you e.g. try to cast to T, it suggests using invariant
in exactly this way:
"Object casts are unsupported. Try 'if ($var instanceof T)' or 'invariant($var instanceof T, ...)'"
Maybe T here refers to an arbitrary type.
I think this will do, as it avoids the need for the factory method:
abstract class ActiveRecord<T>
{
public function get_item_list(): array<T>
{
/* HH_FIXME[4110] Invalid return type */
return [clone $this]; // Result of database query
}
}
The runtime produces the expected result, and it passes the type checker. What's important is that the method has the right signature, array<T>
, so that any code using it may be statically checked.
Yeah you can't do instanceof T
for the same reason you can't do new T
since the runtime doesn't know what T
is due to erasure. Feel free to file a new issue for the misleading error message there.
For your clone $this
example, isn't that returning an array<ActiveRecord<T>>
, not an array<T>
? Creating a new this
is supported, via new static
for example, but isn't the same as the problem in your original post (creating just a new T
).
For your
clone $this
example, isn't that returning anarray<ActiveRecord<T>>
, not anarray<T>
?
If you look at the definition of User
, you'll see it's defined like this:
final class User extends ActiveRecord<User>
Therefore, if you instantiate a User
object, $this in the base class will be of type User
, as will the type variable T
, so it's actually correct. :)
Creating a new
this
is supported, vianew static
for example, but isn't the same as the problem in your original post (creating just a newT
).
Ah, right. In fact, I found that since T
is User
, and $this
is an instance of User
, I can do it like this:
abstract class ActiveRecord
{
public function get_item_list(): array<this>
{
return [clone $this]; // Result of database query
}
}
This way, the class doesn't have to be generic, and no factory method or type constant is needed.
Alternativly, as you suggest, new static()
could be used here, and then <<__ConsistentConstruct>>
needs to be added to ActiveRecord.
.
In modern version of hhvm, you can use type constants and classname. The things you need are:
You can select your database rows and call $entity_classname::fromDBRow($db_row)
.
Hack will understand that the result of this call will be the LSB object type that the record is for.
You do need the intermediary variable $entity_classname
, because static::ENTITY_CLASSNAME::fromDBRow()
is not valid Hack.
function query_fake_database(string $table): vec<dict<string, mixed>> {
switch ($table) {
case 'programming':
return vec[
dict['id' => 1, 'name' => 'HHVM'],
dict['id' => 2, 'name' => 'Hack'],
dict['id' => 3, 'name' => 'XHP'],
];
case 'alphabet':
return vec[
dict['id' => 1, 'letter' => 'A'],
dict['id' => 2, 'letter' => 'B'],
dict['id' => 3, 'letter' => 'C'],
];
default:
invariant_violation('Table %s not defined', $table);
}
}
interface Entity {
public static function fromDBRow(KeyedContainer<string, mixed> $row): this;
}
abstract class ActiveRecord {
abstract const type TEntity as Entity;
abstract const classname<this::TEntity> ENTITY_CLASSNAME;
abstract const string TABLE_NAME;
public function getItemList(): vec<this::TEntity> {
$result = /*await*/ query_fake_database(static::TABLE_NAME);
$entity_classname = static::ENTITY_CLASSNAME;
$out = vec[];
foreach ($result as $row) {
$out[] = $entity_classname::fromDBRow($row);
}
return $out;
}
}
final class ProgrammingRecord extends ActiveRecord {
const type TEntity = ProgrammingEntity;
const classname<this::TEntity> ENTITY_CLASSNAME = ProgrammingEntity::class;
const string TABLE_NAME = 'programming';
}
final class LetterRecord extends ActiveRecord {
const type TEntity = LetterEntity;
const classname<this::TEntity> ENTITY_CLASSNAME = LetterEntity::class;
const string TABLE_NAME = 'alphabet';
}
final class ProgrammingEntity implements Entity {
public function __construct(public int $id, public string $name) {}
public static function fromDBRow(KeyedContainer<string, mixed> $row): this {
return new static($row['id'] as int, $row['name'] as string);
}
}
final class LetterEntity implements Entity {
public function __construct(public int $id, public string $letter) {}
public static function fromDBRow(KeyedContainer<string, mixed> $row): this {
return new static($row['id'] as int, $row['letter'] as string);
}
}
<<__EntryPoint>>
function main(): void {
$programming_record = new ProgrammingRecord();
foreach ($programming_record->getItemList() as $programming_entity) {
\printf("Programming: ID(%d), %s\n", $programming_entity->id, $programming_entity->name);
}
echo \PHP_EOL;
$letter_record = new LetterRecord();
foreach ($letter_record->getItemList() as $letter_entity) {
\printf("Alphabet: ID(%d), %s\n", $letter_entity->id, $letter_entity->letter);
}
}
$hhvm src/activerecord.hack
Programming: ID(1), HHVM
Programming: ID(2), Hack
Programming: ID(3), XHP
Alphabet: ID(1), A
Alphabet: ID(2), B
Alphabet: ID(3), C
I know that Hack's generics are based on type erasure, so the type parameters are removed in the runtime version of the program, but I'm still wondering if there might be a way to accomplish the following.
In our systems, we have a class, ActiveRecord, which represents the pattern with the same name, and which also contitutes an SQL query builder, used to retrieve an array of objects, whose data is read from the database.
Consider the following code:
Here, User is one of many classes inheriting from ActiveRecord, and the get_item_list() method retrieves an array of objects, in this case of the User class. Since get_item_list() can return an array of any kind of (ActiveRecord derived) objects, we can't hardwire "User" in the type declaration like we've done here:
array<User>
Is there a way to accomplish this, statically typed, using Hack?
In C++, using the "Curiously Recurring Template Pattern", you could solve it by passing a template parameter to the base class like this:
class User : public ActiveRecord<User> { ... }
However, since you can't use
new
on a type parameter, or cast to it, in Hack, the following doesn't work:Suggestions welcome. One possibility could be to implement get_item_list() in each subclass, but that leads to unnecessary duplication.
I've also tried having get_item_list() return
array<this>
, but it then complains that "this" is not compatible with "User".