PHPGenerics / php-generics-rfc

Mirror of https://wiki.php.net/rfc/generics for easier collaboration
186 stars 2 forks source link

Type arguments as strings (::class) #17

Open mindplay-dk opened 6 years ago

mindplay-dk commented 6 years ago

What does ::class return for parameterized types?

For example:

class Box<T> {
  public function __construct() {
    var_dump(T::class);
  }
}

$box = new Box<Foo<Bar>>();

Do you expect to see Foo or Foo<Bar> in this case?

Update the "Parameterized Functions and Methods" section with any new information/decisions.

See also #16.

orolyn commented 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> .

mindplay-dk commented 6 years ago

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?

mindplay-dk commented 6 years ago

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.

orolyn commented 6 years ago

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.

morrisonlevi commented 6 years ago

Related: what does self refer to here? Box? Or Box<T>?:

class Box<T> {
  function m(): self {
    return $this;
  }
}
orolyn commented 6 years ago

Even worse:

class Box<T> {
    function static m(): self {
        return new Box<string>();
    }
}
natebrunette commented 6 years ago

I think it should mean Box

tux-rampage commented 5 years ago

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>
    {
        // ...
    }
}
XedinUnknown commented 5 years ago

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.

mecha commented 5 years ago

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.

XedinUnknown commented 5 years ago

Exactly. So, this sounds good to me: