Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dog/Cat/Animal generic snafu should not be allowed #1603

Closed
muglug opened this issue May 3, 2019 · 29 comments
Closed

Dog/Cat/Animal generic snafu should not be allowed #1603

muglug opened this issue May 3, 2019 · 29 comments
Labels

Comments

@muglug
Copy link
Collaborator

muglug commented May 3, 2019

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

/**
 * @template T
 */
class Collection {
    /**
     * @param T $t
     */
    public function add($t) : void {}
}

/**
 * @param Collection<Animal> $list
 */
function addAnimal(Collection $list) : void {
    $list->add(new Cat());
}

/**
 * @param Collection<Dog> $list
 */
function takesDogList(Collection $list) : void {
    addAnimal($list); // this should be an error
}

Expected: InvalidArgument when passing Collection<Dog> to Collection<Animal>
Actual: No issue

@muglug muglug added the bug label May 3, 2019
@iluuu1994
Copy link
Contributor

iluuu1994 commented May 3, 2019

Something like C#s in and out keywords would be necessary to understand if upcasting or downcasting is allowed:

@muglug
Copy link
Collaborator Author

muglug commented May 3, 2019

That's great info, thanks!

This is made a little more complicated/interesting with union types.

Currently Psalm treats MyCollection<A>|MyCollection<B> as identical to MyCollection<A|B>, but that's not right.

@iluuu1994
Copy link
Contributor

Since a mutable collection uses the generic type T as both input and output (set(int index, T value) and get(int index): T) I think strictly speaking T is neither covariant nor contravariant.

class ReadOnlyCollection<out T> {
	get(int index): T
}

class WriteOnlyCollection<in T> {
	set(int index, T value)
}

class Collection<T> {
	set(int index, T value)
	get(int index): T
}

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

let _: ReadOnlyCollection<Animal> = new ReadOnlyCollection<Dog>() // OK
let _: ReadOnlyCollection<Dog> = new ReadOnlyCollection<Animal>() // NOK
let _: ReadOnlyCollection<Dog|Cat> = new ReadOnlyCollection<Dog>() // OK
let _: ReadOnlyCollection<Dog> = new ReadOnlyCollection<Dog|Cat>() // NOK

let _: WriteOnlyCollection<Dog> = new WriteOnlyCollection<Animal>() // OK
let _: WriteOnlyCollection<Animal> = new WriteOnlyCollection<Dog>() // NOK
let _: WriteOnlyCollection<Dog> = new WriteOnlyCollection<Dog|Cat>() // OK
let _: WriteOnlyCollection<Dog|Cat> = new WriteOnlyCollection<Dog>() // NOK

let _: Collection<Animal> = new Collection<Dog>() // NOK
let _: Collection<Dog> = new Collection<Animal>() // NOK
let _: Collection<Dog|Cat> = new Collection<Dog>() // NOK
let _: Collection<Dog> = new Collection<Dog|Cat>() // NOK

Hope this helps.

@muglug
Copy link
Collaborator Author

muglug commented May 6, 2019

Hmm but I also don't want an error with https://psalm.dev/r/2e0c889319

@iluuu1994
Copy link
Contributor

Unfortunately, this should be an error since getSound could alter the collection and add an instance of any other class extending Animal. There are languages that ignore this for convenience but if you want to be safe you'd have to clone the collection or prove that the collection is not altered.

@iluuu1994
Copy link
Contributor

iluuu1994 commented May 6, 2019

You could allow casting Collection<Dog> to iterable<Animal> though since iterable isn't mutable. This would also make getSound more portable.

@muglug
Copy link
Collaborator Author

muglug commented May 6, 2019

Maybe this check could be bypassed when a @psalm-immutable annotation exists on the class?

@muglug
Copy link
Collaborator Author

muglug commented May 6, 2019

Where Traversable and iterable would be @psalm-immutable by default

@iluuu1994
Copy link
Contributor

Maybe this check could be bypassed when a @psalm-immutable annotation exists on the class?

Sounds a little inflexible. It's not just about mutability. in and out restrict where you can use the generic parameters. Specifying out allows upcasting T but it restricts you from using it as a parameter type. in is the opposite. Traversible would be specified internally as Traversible<out T> and iterable as iterable<out T>. That's the approach taken by C#.

@muglug
Copy link
Collaborator Author

muglug commented May 6, 2019

Yeah, I guess I'd just rather have the in/out not just on the params, but on the whole templated class.

So @psalm-template-allow-subtyping or similar, which would apply to all template params.

Sidenote: Hack has SomeContainer<T super Foo> which relates to something different - a constraint on what can be assigned to T

@muglug
Copy link
Collaborator Author

muglug commented May 6, 2019

I have the implementation ready - it's just a matter of the circumstances under which this elseif branch is entered: /~https://github.com/vimeo/psalm/pull/1604/files#diff-6f471193a38ac3da0731e37bc4724239R1608

@iluuu1994
Copy link
Contributor

iluuu1994 commented May 6, 2019

Have you considered this case?

/**
 * @template TKey
 * @template KVal
 * @psalm-template-allow-subtyping
 */
class Collection
{
    /**
     * @param TKey $key
     * @return TVal
     */
    public function get($key)
    {
        // ...
    }
}

/** @var Collection<int, Dog> */
$dogCol = new Collection();
/** @var Collection<mixed, Animal> */
$animalCol = $dogCol;
$animalCol->get(new \stdClass());

How do you know which parameter is covariant/contravariant?

On the other hand, this:

/**
 * @template TKey
 * @template out KVal
 */
class Collection
{
    /**
     * @param TKey $key
     * @return TVal
     */
    public function get($key)
    {
        // ...
    }
}

/** @var Collection<int, Dog> */
$dogCol = new Collection();
/** @var Collection<mixed, Animal> */ // <-- This is invalid
$animalCol = $dogCol;
$animalCol->get(new \stdClass());

would allow Psalm to know that this isn't allowed.

@muglug
Copy link
Collaborator Author

muglug commented May 6, 2019

Ok, you've convinced me it should be per-param - is out the best word?

@iluuu1994
Copy link
Contributor

@muglug Can't think of anything better. covariant would be another option but I think out would be more intuitive for most people.

Are you also planning to error when T is used incorrectly?

https://dotnetfiddle.net/Jdi9mA

One note: C# restricts in and out to interfaces. I don't think there's a technical reason for that other than that it's simply not supported (or it has something to do with the runtime with is irrelevant for Psalm). Also, they do allow using T for parameters of type callable(): T while marking it as out. This seems to be an edge case though.

@muglug
Copy link
Collaborator Author

muglug commented May 6, 2019

I'm hesitant about out because there's already the concept of out parameters, and Psalm has a @param-out annotation that roughly mirrors that functionality.

Covariant sounds better - maybe something like

@psalm-template TKey as Foo
@psalm-template-covariant TValue as Bar

@iluuu1994
Copy link
Contributor

I see. Yeah that makes sense 👍

@iluuu1994
Copy link
Contributor

Something I forgot: There's an RFC for generics in PHP in the works. It looks like they do plan on using the in and out keywords. Just something to keep in mind.

@muglug
Copy link
Collaborator Author

muglug commented May 6, 2019

Oh yeah, if/when that RFC's accepted I'll happily update the syntax!

@muglug muglug closed this as completed in 751253d May 6, 2019
@iluuu1994
Copy link
Contributor

iluuu1994 commented May 6, 2019

This is dope 👍

Only thing left is to warn on line 13

@muglug
Copy link
Collaborator Author

muglug commented May 6, 2019

Only thing left is to warn on line 13

What should the message be, roughly?

@iluuu1994
Copy link
Contributor

Can't use a covariant template as an input

Something like that?

@iluuu1994
Copy link
Contributor

This should also error.

/**
 * @template T
 */
class Invariant
{
}

/**
 * @template-covariant T
 * @extends Invariant<T>
 */
class Covariant extends Invariant
{
}

@muglug
Copy link
Collaborator Author

muglug commented May 6, 2019

Input should be fine for a constructor, though?

https://psalm.dev/r/82914a1e16

@muglug
Copy link
Collaborator Author

muglug commented May 6, 2019

/**
 * @template-covariant T
 * @extends Invariant<T>
 */

Why is that bad? Could you provide an example?

ArrayObject and others currently have non-covariant parameters, and it might be annoying to prevent people downstream from extending them with covariant params.

@iluuu1994
Copy link
Contributor

Yeah, constructors should be fine. Only public non-static methods methods should be relevant I think. I'll have to think more about casting in private methods.

@iluuu1994
Copy link
Contributor

Why is that bad? Could you provide an example?

Hope this is understandable:

https://psalm.dev/r/6d69fd7baf

<?php

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

/**
 * @template T
 */
class InvariantBox
{
    /**
     * @var T|null
     */
    protected $animal;
    
    /**
     * @param T $value
     */
    public function set($value): void
    {
        $this->animal = $value;
    }
}

/**
 * @template-covariant T
 * @extends InvariantBox<T>
 */
class CovariantBox extends InvariantBox
{
}

/** @var CovariantBox<Dog> $dogBox */
$dogBox = new CovariantBox();
/** @var CovariantBox<Animal> $animalBox */
$animalBox = $dogBox;
$animalBox->set(new Cat());

Basically, we need to make sure that the inherited API also doesn't use T as an parameter type.

@muglug
Copy link
Collaborator Author

muglug commented May 6, 2019

Ah that makes sense - mind creating tickets for both, so I don't lose track?

@iluuu1994
Copy link
Contributor

Done

@muglug
Copy link
Collaborator Author

muglug commented May 6, 2019

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants