PHPGenerics / php-generics-rfc

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

Parameterized Methods #20

Open mindplay-dk opened 6 years ago

mindplay-dk commented 6 years ago

The "Parameterized Methods" section talks about extending/overriding parameterized methods.

The example may be too elaborate and distracting? The point of the example and two last paragraphs is a bit unclear.

Consider using a better example and improving the description.

orolyn commented 6 years ago

Just to through in a question regarding this:

class Box<T>
{
    public function __construct(T $content)
    {
        // ...
    }

    public function<T> doSomething()
    {

    }
}

So you can see here that the method doSomething declared T, which is also a type parameter of the class. My initial reaction to this is that T on the method is overriding T on the class, so the T in Box<T> effectively dissappears.

Alternatively, this could throw an error because there is a conflict. Personally I don't mind which, but its worth choosing one for the RFC.

morrisonlevi commented 6 years ago

I would check how other languages handle this, but my inclination is to error here. It seems error-prone to allow it.

marcospassos commented 6 years ago

I just tested it in Java and it works by overriding the type parameter of the class.

From Java docs:

We use T for type, whenever there isn't anything more specific about the type to distinguish it. This is often the case in generic methods. If there are multiple type parameters, we might use letters that neighbor T in the alphabet, such as S. If a generic method appears inside a generic class, it's a good idea to avoid using the same names for the type parameters of the method and class, to avoid confusion. The same applies to nested generic classes.

I share of the same opinion as @morrisonlevi about reporting a compile-time error.

marcospassos commented 6 years ago

I was wondering, how are we supposed to handle the following cases?

interface Evaluator<N instanceof Node, V> {
    public function supports(N node) : bool;
    public function evaluate(N node) : V;
}

class Context {
    private $evaluators;
    public function __construct(Evaluator ...$evaluators) {
        $this->evaluators = $evaluators;
    }
    public function <V> evaluate(Node $node) : V {
         $evaluator = $this->getEvaluator($node);
         $result = $evaluator->evaluate($node);

         return $result;
    } 
}

In Java, we'd cast the result to ensure the return type. In PHP, I believe we need some mechanism to check 1) whether a type matches another and 1) if a value matches a type.

$reflectionTypeA = $relectionParameterA->getType();
$reflectionTypeB = $relectionParameterB->getType();
// 1)
$reflectionTypeA->supports("foo");
// 2)
$reflectionTypeA->isSubtypeOf($reflectionTypeB);

Using reflection for introspection looks ok, but seems a lot of code for just providing better feedback about the unexpected return type.

orolyn commented 6 years ago

@marcospassos You would define Context in one of the following ways:

class Context {
    private $evaluators;
    public function __construct(Evaluator<Node, string> ...$evaluators) {
        $this->evaluators = $evaluators;
    }
    public function evaluate(Node $node) : string {
         $evaluator = $this->getEvaluator($node);
         $result = $evaluator->evaluate($node);

         return $result;
    }
}

// or

class Context<V> {
    private $evaluators;
    public function __construct(Evaluator<Node, V> ...$evaluators) {
        $this->evaluators = $evaluators;
    }
    public function evaluate(Node $node) : V {
         $evaluator = $this->getEvaluator($node);
         $result = $evaluator->evaluate($node);

         return $result;
    }
}

This way you know what the return value should be. But even in your example where you define the method type parameter V the caller of $context->evaluate<string>() is already aware of what the return value should be.

marcospassos commented 6 years ago

@orolyn if you know the result type in beforehand, this is the way to go, but this is not often the case. In a AST evaluation, the return type of BooleanEvaluato::evaluate() : bool is known, but of MemberAccessEvaluator::evaluate() : mixed is not.

The following snippet is based on another example from the JDK:

interface Temporal {
    <R> R query(TemporalQuery<R> query);
}

interface Query<R> {
    R queryFrom(Temporal temporal);
}

class CustomQuery<R extends Temporal> implements Query {
    R queryFrom(Temporal temporal) {
           return (R) temporal.methodThatReturnsATemporal();
    }
}

LocalDate date = temporal.query(customQuery);

Back in PHP, the interpreter would throw a TypeError giving no chance for providing better feedback or a more sophisticated logic to filter out evaluators whose return type does not match the parameter type. As it stands, I can only see the usefulness of generics methods as generic factory methods:

class Factory {
    public function <T> create() : Foo<T> {
         return new Foo<T>(...);
    }
}
orolyn commented 6 years ago

@marcospassos I may have misunderstood your question a little in regards to where you'd be making that check. But I think I see where you're coming from now.

Is it here you're referring to?

public function evaluate(Node $node) : V {
    $evaluator = $this->getEvaluator($node);
    $result = $evaluator->evaluate($node);

    // want to make sure $result is V

    return $result;
}

If so, then you have a few options including:

public function <V> evaluate(Node $node) : V {
    $evaluator = $this->getEvaluator($node);
    $result = $evaluator->evaluate($node);

    // gettype() returns values like: `"integer"` and `"double"` etc, so we can't use it.
    switch (true) {
        case is_int($result) && V !== 'int':
        case is_float($result) && V !== 'float':
        case is_string($result) && V !== 'string':
        case is_array($result) && V !== 'array':
        case is_object($result) && (!$result instanceof V):
            throw new InvalidEvaluationResultException($evaluator, $node);
    }

    return $result;
}

try {
    $result = $context->evaluate<string>($node);
} catch (InvalidEvaluationResultException $e) {
    ...
}

This does make the assumption that V can be read as a string, however $result instanceof V is certainly possible. Its a bit much, and perhaps a new built in function similar to is_a such as is_of_type($result, V) should be included for cases like this.

marcospassos commented 6 years ago

That's the point, boilerplate code everywhere.

marcospassos commented 6 years ago

This does make the assumption that V can be read as a string, however $result instanceof V is certainly possible. Its a bit much, and perhaps a new built in function similar to is_a such as is_of_type($result, V) should be included for cases like this.

Btw, how type parameters conflicting with constants are resolved?

morrisonlevi commented 6 years ago

Btw, how type parameters conflicting with constants are resolved?

They aren't. The proposed usages such as is_of_type($result, V) are invalid. Type parameters can only be used in places where type names are expected. Add ::class to put it into a type name usage.

orolyn commented 6 years ago

@morrisonlevi

Does that mean where T is string then T::class might return "string"?

morrisonlevi commented 6 years ago

Yes. In case you are worried about it, note that string::class is permitted today.

orolyn commented 6 years ago

Nope, thats awesome.

marcospassos commented 6 years ago

To summarize, it means that either V::class === Foo::class or $foo instanceof V::class will be supported, right? It makes me wonder about how it will work with generic types. If V is Foo<Bar> it implies that V::class === 'Foo<Bar>' should evaluates to true, right?

natebrunette commented 6 years ago

The class is Foo, the type is Foo<Bar>

marcospassos commented 6 years ago

So, how one can check if an object is an instance of a given type?

natebrunette commented 6 years ago

We haven’t figured that out, I think we should add a ::type class constant

marcospassos commented 6 years ago

It looks like a good idea. For any non-generic subject, subject::class will be always equal to subject::type.

marcospassos commented 6 years ago

In that sense, does $subject instanceof V::type should work?

natebrunette commented 6 years ago

Yeah, theoretically. We haven’t discussed instanceof at all as far as I can remember. I’m not sure how difficult changing it is. cc @morrisonlevi

natebrunette commented 6 years ago

@marcospassos I detail some of this in #27

marcospassos commented 6 years ago

A handy feature in Java is binding arguments types to method parameters. It sounds especially useful for a dynamically-typed language like PHP.

interface Bar<T> {
    public function getValue() : T;
}

class StringBar implements Bar<string> {
    private string $value;

    public function __construct(String $value) {
         $this->value = $valuel
    }

   public function getValue() : string {
       return $this->value;
    }
}

function <T> foo(Bar<T> $bar) : T {
    return $bar->getValue();
}

// Inferred "foobar"
echo foo(new Bar<string>("foobar"));
// Alternatively
echo foo<string>(new Bar<string>("foobar"));
// Type error
echo foo<int>(new Bar<string>("foobar"));

Will this feature be supported?

natebrunette commented 6 years ago

@marcospassos We will be inferring types, but we will not be handling generic methods or functions for the first version.