Open mindplay-dk opened 6 years ago
If you think about how variables are used to instantiate classes:
$class = ... // some source
Then in a dynamic setting its impossible for the developer to know what type arguments to use. While this could theoretically be supported:
$class = 'Dictionary';
new $class<string, int>();
It isn't as useful as:
$class = 'Dictionary<string, int>';
new $class();
And so that falls back on instantiating strings as class names:
$class = Box<Foo<Bar>>::class;
new $class();
However, on T
itself, I would say that T::class
would throw an error, or return a non-null value when it isn't actually a class name, and in all other cases would ideally return Foo<Bar>
so you may still dynamically reference the same permutation.
In regards to reflection, Box<Foo<Bar>>
is related to, but not functionally the same as Box
because they wouldn't be compatible if you tried to use them the same way. If you then only reflected on Box
, then Box
could be considered the equivalent of Box<void>
or Box<mixed>
, where this would be valid in all instances with the exception where Box<T is BaseClassOrInterface>
.
In my opinion, type-parameters represented as strings is trouble.
If we had that, I'd expect consistency, e.g. generic class-descriptions to be returned by get_class()
, to work with the ReflectionClass
constructor, etc. - which would be pretty bad in terms of performance, but perhaps more importantly would be a major BC break, as, currently, no code is written with the expectation that get_class()
or reflected class-names contain anything other than \
-delimited names.
I think we need a different representation - a reflection model of some sort.
I also feel like the examples are too unrealistic to serve as a basis for that discussion - for example:
$class = 'Dictionary<string, int>';
new $class();
Where would this string come from in the first place?
Can we maybe find a real-world example, like a DI container or ORM etc., where this might actually come into play? I think, in most of these cases, in practice, objects get instantiated via reflection rather than from class-names in strings?
Things like get_class()
and new $class()
are essentially a form of low-key, poor-man's reflection - it's not something you see very often in the wild, and probably something you'd use only in rare cases where you know at least something about the type you're instantiating, even if some part of that type is dynamic?
I will just note here what I pointed out in the meeting after @nikic and @morrisonlevi left.
I seem to have a slightly different view of what type-arguments to a class constructor are - to me, they're just a specific kind of constructor argument, just like other constructor arguments.
In a templated language like C++ I would look at this differently, but in a dynamic, reified/reflected language like PHP, I expect type arguments are just a special kind of value that gets stored in the instance.
I think this is partially about expectations.
If today I have a non-generic class Box
and create an instance $box = new Box("hi")
, I don't expect get_class($box)
to tell me anything about the type of value stored in that instance.
In the same way, if I have a generic class Box<T>
and create an instance $box = new Box<string>()
, I also don't expect get_class($box)
to include the type argument - I expect I'll have to provide the type arguments to create one, just like any other constructor arguments.
I don't so much view Box<string>
as being a type (and this probably comes back to what @natebrunette was talking about in #27) but rather, to me, Box
is the type, and string
is additional information that belongs to the instance.
I understand that this holds only for generic classes, since e.g. generic interfaces don't have an instance to store these parameters in - but, in that case, I expect that something like class StringList extends List<string> {}
has that same kind of type-information stored in the class StringList
.
If I look at @morrisonlevi's plain PHP implementation of "generics", where the type-arguments are actually values that get stored, that's very much how I think about it working internally in languages like Dart, and how I imagine it would work in PHP.
So I wouldn't expect something like this work:
$class = 'Dictionary<string, int>';
new $class();
But I would expect this to work: (maybe)
$class = 'Dictionary';
new $class<string, int>();
In a sense, if get_class()
returned Dictionary<string, int>
, that's a serialized copy of the type-arguments - and I wouldn't expect that at all. I'd expect serialize
#29 would do that, but from get_class()
, I'd expect just the class-name.
OK, So I've just tested this in C#.NET with:
using System;
using System.Collections.Generic;
namespace test
{
class MainClass
{
public static void Main (string[] args)
{
Dictionary<Int32, String> dictionary = new Dictionary<Int32, String>();
Console.WriteLine (dictionary.GetType().Name);
Console.WriteLine (dictionary.GetType().GenericTypeArguments[0]);
Console.WriteLine (dictionary.GetType().GenericTypeArguments[1]);
}
}
}
And the output was:
Dictionary`2
System.Int32
System.String
With GenericTypeArguments
being a reflective property. So I concede that the name shouldn't include the type arguments.
Related: what does self
refer to here? Box
? Or Box<T>
?:
class Box<T> {
function m(): self {
return $this;
}
}
Even worse:
class Box<T> {
function static m(): self {
return new Box<string>();
}
}
I think it should mean Box
Even worse:
class Box<T> { function static m(): self { return new Box<string>(); } }
It should be Box<T>
and the code should throw a TypeError
. Since generics are there to template a type. And the purpose of self
is to hint the type of it's context. For generics this is the parameterized type.
What you are describing is a case where you will need generic functions or express that this method is not respecting the type parameter purposefully:
class Box<T>
{
static function m(): Box<string>
{
return new Box<string>();
}
static function b<Other>() : Box<Other>
{
// ...
}
}
A "permutation" of a type in the context of generics is definitely a different type. The whole concept is all specifically about types, and will be used for that. The <T>
in this example is not a constructor argument at all, like @mindplay-dk has pointed out in relation to interfaces. I think that whether interpreted or compiled, the "templating" aspect remains: it is there to show us a similarity between different things that work in a similar way, such that the type difference doesn't impact the generic principle; and we need this so that we don't have to create a large amount of interfaces for these similar things. Besides the Box
or ContainerInterface
example, one prominent case is the factory pattern. In an effort to standardize factories, I created a FactoryInterface
. But of course, without sub-types such as the ContainerFactoryInterface
, it is useless: there's little advantage to knowing that something is a factory without knowing what that factory makes. IMHO this becomes especially apparent when having a GenericCallbackFactory
, i.e. something that allows easy conversion of a callback into a standard factory: using the PHP return type hints, proper type-safety could be achieved.
IMO, the above illustrates very clearly that the "type-argument" is more about type than argument, and the fact that it is applied in the constructor is incidental.
To reuse the example, to me Box
is not a real class. I would expect the declaration of Box<T>
to not create an actual class, but merely give PHP enough information to be able to create a class later.
If my code attempts to instantiate Box<string>
, PHP has enough information stored about Box<T>
to create an actual class where T
is substituted by string
, given that such as class was not already previously generated. Sounds awfully similar to autoloading. In fact, one can already implement an autoloader that loads JIT-generated classes. I've actually done this in the past.
So I'd say Box
is not a type, no more that a trait is a type. But the concrete classes that are generated from it are. Also considering how ::class
works and how it can be used, it only follows that it should return a usable type to keep it consistent with non-generic types.
Exactly. So, this sounds good to me:
self
is the permutation, as it is only used inside the class, and most often to create a new instance. The static methods and constants will be the same for all permutations, so that can work consistently.::class
returns the permutation, because it is mostly used outside of the class, and is useful for mappings.get_class()
would return the "template" name, e.g. Box
. A 2nd arg could be introduced which, if not default, will return the permutation name. Alternatively, another function could be added for that.
What does
::class
return for parameterized types?For example:
Do you expect to see
Foo
orFoo<Bar>
in this case?Foo
makes this string unusable as a unique key, for example, in a DI container that stores entries under the type-name: e.g.Foo<Bar>
collides withFoo<Baz>
.Foo<Bar>
makes this string unusable with e.g.new ReflectionClass()
or forces us to consider parsing type-arguments and reflecting every permutation of a parameterized class as a new instance ofReflectionClass
. (which raises further questions about reflecting on the parameterized class itself, rather than a reified instance; questions that we may need to answer anyhow, but...)Update the "Parameterized Functions and Methods" section with any new information/decisions.
See also #16.