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

Brainstorming easier type inference #1

Closed
natebrunette opened this issue Jun 8, 2018 · 6 comments
Closed

Brainstorming easier type inference #1

natebrunette opened this issue Jun 8, 2018 · 6 comments

Comments

@natebrunette
Copy link
Collaborator

I've been trying to figure out how to create an easier way to provide type information to methods as an argument, similar to how Java can use MyClass.class to return a Class<T>. This is basically the same as PHP's ReflectionClass if you're unfamiliar.

I don't necessarily want to start typing to ReflectionClass<T> or change the signature of ReflectionClass, but that's an option.

I'm not sure if it's possible, but it would be great if we could use a constant to return an instance of the required type, something like MyClass::type. I would definitely want that instantiation to be lazy, and I'm not sure if that's possible in PHP.

Perhaps, there could be a new mini reflection class (e.g. ClassToken<T>) that could provide a minimal amount of information about the class signature (name, type parameters, etc).

What this solves

A good example of what this could help with is a dependency container.

For example, a container could have this method:

public function get(string $name)
{
    return $this->dependencies[$name] ?? null;
}

$container->get(MyDependency::class);

We could still derive value from using generics:

public function get<T>(string $name): T
{
    return $this->dependencies[$name] ?? null;
}

$container->get<MyDependency>(MyDependency::class);

But this is a little long. Ideally, we could do something like this instead:

public function get<T>(ClassToken<T> $token): ?T
{
    return $this->dependencies[$token->getClassName()] ?? null;
}

$container->get(MyDependency::type);

MyDependency::type would return a ClassToken<MyDependency>

@mindplay-dk
Copy link
Collaborator

mindplay-dk commented Jun 8, 2018

I think about DI containers in this context a lot, it's a meaningful use-case 👍

What I envisioned for my own DI container is something along the lines of this:

class Container
{
    public function get<T = mixed>(string $name = null): T
    {
        return $this->dependencies[$name ?? T::class];
    }

    // ...
}

// examples:

$service = $container->get<MyService>(); // singleton component

$pdo = $container->get<PDO>("connection_name"); // named component

$foo = $container->get("foo"); // named component, no type-check (T defaults to mixed)

That is, you can optionally specify the component name statically with a type-argument, for singleton services, or you can specify the component name yourself to get a specific instance - or both, specify a component name as well as the expected type, for better static analysis plus run-time type-checking of the returned component.

I'd like the type alias T to work consistently with type-hints in PHP now, e.g. with T::class resolving to the actual class-name. (This may be an assumption I didn't describe in the spec?)

Default type-arguments are described in the spec, only (as noted in the spec as well) there's presently no way to type-hint as mixed in PHP, so it looks like we may have to add that. (I think maybe there's been talk of adding a formal mixed type-hint to PHP anyway.)

I would definitely want that instantiation to be lazy, and I'm not sure if that's possible in PHP.

It's definitely possible in this particular case - many things are type-hinted in PHP and doesn't trigger auto-loading, for example function foo(Bar $bar) {} does not trigger auto-loading of Bar, even when invoked... what triggers auto-loading is class X extends Bar which depends on the implementation - type-hinting and explicit type-checks like $bar instanceof Bar can be checked internally simply by seeing if Bar is even loaded, which, if it's not, $bar can't possibly implement Bar.

In summary, type-hints in PHP exist only as strings in-memory until the implementation of the type is required. It's really important that the implementation of generics sticks to that pattern as closely as possible.

For example:

function is_a<T>($value): bool {
    return $value instanceof T;
}

$foo = new Foo();

var_dump(is_a<Bar>($foo)); // => false

This should not trigger auto-loading of Bar.

Similarly:

use Foo\Bar;

function get_type<T>(): string {
    return T::class;
}

var_dump(get_type<Bar>()); // => "Foo\Bar"

This should also not trigger auto-loading of Bar.

In both cases, all that is required is the name of the type, and not the implementation.

@natebrunette
Copy link
Collaborator Author

natebrunette commented Jun 8, 2018

🤦‍♀️

Yeah, T::class would be perfect. I also considered making the parameter optional, but I wasn't sure how to get the class name without reflection. I believe that per the current version of the RFC, ::class would return the name without any type parameters. I think we'd also need a way to get the full name, but that could perhaps be a different class constant.

The part about the lazy instantiation referred to MyClass::type returning an instantiation of ClassToken<T>, but that's likely not needed, which makes things a lot simpler.

I don't think we need a mixed type because a generic <T> should mean any type.

EDIT
Now I understand your point about default type parameter values and mixed. Are there other languages that support defaults?

@mindplay-dk
Copy link
Collaborator

I think we'd also need a way to get the full name, but that could perhaps be a different class constant.

The ::class constant does return the full name, consistent with how ::class works on types.

See the get_type example above, which had a small typo.

I don't think we need a mixed type because a generic <T> should mean any type.

Yes, but how will you specify that "any" (in php-doc "mixed") as a default?

That is, we need to support both optional and required type-arguments:

function foo<T = mixed>() {} // optional type-argument

function foo<T>() {} // required type-argument

The default for type-arguments should be required - specifying a default of mixed for an optional type-argument with type-constraint is an explicit way to make it optional.

The point is, type arguments need to always refer to a type - if they're optional, they must default to something. They can't be null, as null is not a type, and that would create a pretty serious inconsistency, since you can't count on ::class to consistently return a string, which creates edge-cases.

Note that there's already edge-cases in PHP, because scalar types like string, int etc. aren't really type-names, but pseudo-types - you already need to handle those explicitly when working with reflection. This could maybe be addressed someday though, e.g. if string and int object-types are ever introduced - there has been talk of that.

I think it's important we don't introduce yet another inconsistency here, so I don't think there's any sound way around adding a mixed type.

@natebrunette
Copy link
Collaborator Author

How would ::class work on a class with type parameters?

class MyClass<T> {}

MyClass::class; // -> ?

Based on the RFC, I assume that it returns MyClass, but it would be valuable to be able to get access toMyClass<T>.

Default type parameter values
My suggestion here is to always make type parameters required, either specified or inferred. My reasoning is that Java and C# require them, and it reduces the complexity of the feature.

Additionally, specifying a parameter as mixed defeats the purpose of having generics in the first place. You could just as easily leave it untyped.

Do you have any strong reasons for why default values should be supported?

@mindplay-dk
Copy link
Collaborator

How would ::class work on a class with type parameters?
Based on the RFC, I assume that it returns MyClass, but it would be valuable to be able to get access to MyClass<T>.

I thought so early on while writing the RFC, but I think there are only rare and marginal use-cases for that - maybe as keys (for example in a registry or DI container) but probably not much else.

There should be a way to get a string like MyClass<MyType> for an instance, via the reflection API, but it shouldn't be the default, and definitely doesn't make sense for ::class, as this would break e.g. Composer (and most auto-loaders) which maps the class-name to a file-name.

Generating strings such as MyClass<T> for a class-reflection, or MyClass<MyType> for an instance, for diagnostic/debugging purposes (and possibly for use as keys in maps etc.) should be possible - but we really want to avoid creating a scenario where somebody would need to parse such a string and try to turn it back into a type or instance.

And we want to make sure that e.g. ::class and get_class() etc. works consistently with how it works today, e.g. meeting the same expectations in terms of what those strings look like, where and how they can be used, etc.

My suggestion here is to always make type parameters required, either specified or inferred

Absolutely 👍

Additionally, specifying a parameter as mixed defeats the purpose of having generics in the first place. You could just as easily leave it untyped.

There's a subtle difference: "untyped" would imply that there's a null value for the type-hint or something, which would be unacceptable.

I also think that mixed as a default is definitely also unacceptable - I was trying to say that in my previous comment.

However, disallowed mixed (or something like it) is going to create problems, because PHP doesn't have a common supertype of all built-in types. That is, string and int etc. belong to an entirely different type-hierarchy than object types, and there's for formal common ancestor.

It's an inconsistency of the language, but one that we have to deal with - so, where other languages have e.g. Object as a common super-type of all possible types, we have to invent something similar, the closest known concept being that of mixed from php-doc. (which also had to invent it.)

If we don't support something like mixed, we make it impossible to opt out of type-checks e.g. in collection-types - there's no reason we should impose arbitrary constraints and make it impossible to construct a collection-instance that isn't type-checked.

If we don't allow something like mixed, we're essentially forcing someone to pick between scalar, resource or object types - PHP being as dynamic as it is, it's important not to introduce new "opinionated" constraints that don't apply to the rest of PHP, where e.g. arguments, return-types, variables and properties are all be "mixed" by default.

Do you have any strong reasons for why default values should be supported?

You mean default type-arguments?

They're sometimes convenient, when you have a likely use-case for a generic class and you'd like to provide a default. In other words, this is primarily for convenience. (I write a lot of Typescript, and have found defaults to be useful from time to time.)

I don't think there's any reason not to support this feature?

You can read about some of the reasons people wanted this feature in Typescript here.

@natebrunette
Copy link
Collaborator Author

Ok, thanks for your feedback!

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

No branches or pull requests

2 participants