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

Implement Parameter Type Widening RFC #2265

Closed
wants to merge 1 commit into
base: master
from

Conversation

@kelunik
Contributor

kelunik commented Jan 1, 2017

@php-pulls

This comment has been minimized.

php-pulls commented Jan 1, 2017

Comment on behalf of krakjoe at php.net:

labelling

@php-pulls php-pulls added the RFC label Jan 1, 2017

@kelunik kelunik changed the title from Implement Parameter No Type Variance RFC to Implement Parameter Type Widening RFC Jan 13, 2017

@kelunik

This comment has been minimized.

Contributor

kelunik commented Jan 15, 2017

Rebase onto current master with the type changes, the test failure is unrelated.

@skie

This comment has been minimized.

skie commented Jan 15, 2017

Of course, send SOLID to hell. Who is Barbara Liskov??? Some mad woman? Of course! Let's allow to break this principles and ideas!

}
if (!ZEND_TYPE_IS_SET(proto_arg_info->type)) {
/* Child defines a type, but parent doesn't, violates LSP */

This comment has been minimized.

@nikic

nikic Jan 15, 2017

Member

I think it would be more intuitive to move this these checks into the argument checking code, rather than have it in the code that is shared between argument and return types. It works either way because return type checking only invokes the function if both types are set, but it's a bit confusing.

This comment has been minimized.

@kelunik

kelunik Jan 15, 2017

Contributor

Dunno, it's the place where it was before, so I left it there.

This comment has been minimized.

@guilhermeblanco

guilhermeblanco Jan 16, 2017

This requires decoupling into argument check and return type check, otherwise we'll be allowing widening at return types (basically adding contra-variance support), which is not desired.
Return types only support co-variance.
Argument types only support contra-variance.

This comment has been minimized.

@kelunik

kelunik Jan 16, 2017

Contributor

I'll add test cases to check that it doesn't allow contravariant return types and modify as needed.

@nikic

This comment has been minimized.

Member

nikic commented Jan 15, 2017

@skie If you're wondering why everyone is confused about your comment: The Liskov substitution principle explicitly allows contravariance on method argument types, which is what is being implemented here. The previous implementation was too restrictive, forbidding some cases that were perfectly valid under LSP.

@b1rdex

This comment has been minimized.

b1rdex commented Jan 16, 2017

@nikic sure, contravariance must be allowed. But how did complete removing of type hint became possible and accepted?

@b1rdex

This comment has been minimized.

b1rdex commented Jan 16, 2017

@kelunik your arguments in rfc are totally mad. Enforcing type hint on method signature is breaking change and library must define new major version in this case. Depended libraries must upgrade extending/usages code and then define new self major version. That's how it must work in semver with bc-compability in mind.
Don't know why would you change language to permit totally wrong use case.

@kelunik

This comment has been minimized.

Contributor

kelunik commented Jan 16, 2017

@b1rdex No, it's not a breaking change if your code documented the accepted types before and did these checks manually.

@b1rdex

This comment has been minimized.

b1rdex commented Jan 16, 2017

@kelunik

This comment has been minimized.

Contributor

kelunik commented Jan 16, 2017

Please explain where it breaks.

@nikic

This comment has been minimized.

Member

nikic commented Jan 16, 2017

@b1rdex It may help you to think of removing a type as specifying a (non-existing) mixed type instead, where mixed is the type semilattice supremum. As such, it is always contravariant (pretty much by definition).

@b1rdex

This comment has been minimized.

b1rdex commented Jan 16, 2017

@nikic good catch.

For anyone interested: following snippet is valid in Java 😮 tested with https://www.compilejava.net/

public class Foo {
    public Foo testParentClass(int $foo) {return new Foo();}
}
public class Bar extends Foo {
    public int testParentClass(String $foo) {return 2;}
}
@b1rdex

This comment has been minimized.

b1rdex commented Jan 16, 2017

@kelunik

Changing signature is always breaking change.
Please explain where it breaks.

in php changing type hints on public api will break extenders before this got merged :)

@nikic

This comment has been minimized.

Member

nikic commented Jan 16, 2017

@b1rdex The Java case is somewhat different, because it's not overriding the method, but adding an overload with a different signature. In languages with method overloading variance considerations usually only come in for generic parameters and return types.

@b1rdex

This comment has been minimized.

b1rdex commented Jan 16, 2017

@nikic shame on me, I totally forgot about polymorphism… Hope php will see it someday.

@kelunik

This comment has been minimized.

Contributor

kelunik commented Jan 16, 2017

@b1rdex I don't think method overloading is something we'll see in PHP, at least not in the near future. It's rather complicated with weak types and comes with significant overhead IIRC.

in php changing type hints on public api will break extenders before this got merged :)

  1. Changing a type declaration in a subclass will still not be allowed. The only thing that's allowed is the parent having a restriction and the child not having one, thus accepting the same thing the parent accepted and more.
  2. Being able to upgrade existing type hints in docblocks + manual exceptions to use type declarations without breaking backwards compatibility is an important part to move forward with stricter typing.
  3. It didn't break before, it "just" emitted a warning, because PHP can't verify co- / contravariance at compile time currently, because type declarations do not invoke the autoloader. And we don't have method overloading. So all we can do is allowing co- / contravariance instead of method overloading.
@artemzakholodilo

This comment has been minimized.

artemzakholodilo commented Feb 2, 2017

`abstract class Person
{
private $name;

public function setName($name)
{
    $this->name = $name;
}

public function getName()
{
    return $this->name;
}

protected function __toString()
{
    
}

}

class Foo
{
public function sayHi(Person $person)
{
echo "Hi " . $person->getName();
}
}

class Human extends Person
{}

$foo = new Foo();
$foo->sayHi('Artem');
`

What about this?

@schnittstabil

This comment has been minimized.

schnittstabil commented Feb 2, 2017

@artemzakholodilo This PR won't affect your example; $foo->sayHi('Artem'); stills fails, because 'Artem' is not a Person instance:

Warning: The magic method __toString() must have public visibility and cannot be static […]

Fatal error: Uncaught TypeError:
Argument 1 passed to Foo::sayHi() must be an instance of Person, string given, […]

Stack trace:
#0 […].php(34): Foo->sayHi('Artem')
#1 {main}
  thrown in […]
@kelunik

This comment has been minimized.

Contributor

kelunik commented Feb 3, 2017

@guilhermeblanco @nikic I've improved the implementation to use a separate function for args that does the additional checks. I've added an assertion to the current function, so it's only called if both types are set.

if (!ZEND_TYPE_IS_SET(proto_arg_info->type)) {
/* Child defines a type, but parent doesn't, violates LSP */
return 0;
}

This comment has been minimized.

@nikic

nikic Feb 3, 2017

Member

nit: Some space (instead of tab) indents in the above blocks

This comment has been minimized.

@kelunik

kelunik Feb 3, 2017

Contributor

Should I put them into a separate commit? The rest of the file already has trimmed whitespace on empty lines.

This comment has been minimized.

@nikic

nikic Feb 3, 2017

Member

As this is newly introduced code, it should be in the same commit.

This comment has been minimized.

@nikic

nikic Feb 3, 2017

Member

Sorry, misunderstanding. I was referring to use of 4-space indentation instead of tabs in the lines above, not the the removal of trailing whitespace, that's fine.

This comment has been minimized.

@kelunik

kelunik Feb 3, 2017

Contributor

Ah, right, fixed. I didn't fix the other places using spaces that are not new code.

@nikic nikic self-assigned this Feb 3, 2017

/* Only one has a type declaration and the other one doesn't */
return 0;
}
ZEND_ASSERT(ZEND_TYPE_IS_SET(fe_arg_info->type) && ZEND_TYPE_IS_SET(fe_arg_info->type));

This comment has been minimized.

@bwoebi

bwoebi Feb 4, 2017

Contributor

must be proto_arg_info->type once

This comment has been minimized.

@kelunik

kelunik Feb 4, 2017

Contributor

Good catch, fixed.

@nikic

This comment has been minimized.

Member

nikic commented Feb 4, 2017

Merged via 2edc3cf with an UPGRADING note. Thanks!

@greg0ire greg0ire referenced this pull request Feb 19, 2017

Merged

Add type hinting #65

@JuJuDropThor

This comment has been minimized.

JuJuDropThor commented May 22, 2017

Are you mad ? Why do you want to violate SOLID and Liskus Substitution like that ? Do you really think that kind of thing will improve the reputation of PHP ?

Shame.

@kelunik kelunik deleted the kelunik:parameter-no-type-variance branch May 22, 2017

@kelunik

This comment has been minimized.

Contributor

kelunik commented May 22, 2017

@JuJuDropThor This does not violate the Liskov substitution principle (LSP).

@JuJuDropThor

This comment has been minimized.

JuJuDropThor commented May 22, 2017

So could you tell me, please, how that could be possible if you change declaration of your method between a type and a subtype ?

@kelunik

This comment has been minimized.

Contributor

kelunik commented May 22, 2017

@JuJuDropThor

This comment has been minimized.

JuJuDropThor commented May 22, 2017

Oh array covariance, great...

You have an array parameter:
public function foo(array $foo)

In the subclass, you could have... everything you want.
public function foo($foo)

Your subclass EverythingClass can't be replaced by an object of type ArrayClass without violating LSP.

First line of the wiki you linked:
Many programming language type systems support subtyping. For instance, if Cat is subtype of Animal, then an expression of type Cat can be used whenever an expression of type Animal could. Variance refers to how subtyping between more complex types (list of Cats versus list of Animals, function returning Cat versus function returning Animal, ...) relates to subtyping between their components.

Well, do as you wish, that RFC is validated anyway. That kind of thing doesn't honor PHP repuration.

@pmmaga

This comment has been minimized.

Contributor

pmmaga commented May 22, 2017

In this case the contravariance is not the example you mentioned but exactly the other way around. Assuming your EverythngClass, ArrayClass example, and that ArrayClass is the parent and EverythingClass is the child, EverythingClass can replace ArrayClass without breaking LSP. In the example you are giving, you are trying to use the parent to replace the child which is not valid.

@kelunik

This comment has been minimized.

Contributor

kelunik commented May 22, 2017

Right, an EverythingClass can't be replaced with an ArrayClass, because that's the wrong way round. EverythingClass is the subtype, not the parent type. Anything that accepts an ArrayClass also works perfectly fine with an EverythingClass, because EverythingClass handles arrays just fine, but also everything else.

@php php locked and limited conversation to collaborators May 22, 2017

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.