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

polymorphic callables #5471

Closed
marcosh opened this issue Mar 24, 2021 · 8 comments
Closed

polymorphic callables #5471

marcosh opened this issue Mar 24, 2021 · 8 comments

Comments

@marcosh
Copy link
Contributor

marcosh commented Mar 24, 2021

I would like to define, accept as parameters and return polymorphic functions like

/**
 * @template A
 * @var callable(A): A $id
 */
$id = fn($x) => $x

If I try to provide them with types using parameters, I get errors (see https://psalm.dev/r/35c4a4d34e for some examples). It looks like Psalm is not recognising the @template declaration

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/35c4a4d34e
<?php

/**
 * @template T
 * @var callable(T): T $f
 * @param T $t
 * @return T
 */
$f = fn($t) => $t;

$f(1);

class Foo
{
    /**
     * @template A
     * @var callable(A): A
     */
    private $bar;
    
    /**
     * @template A
     * @param callable(A): A $bar
     */
    public function __construct(callable $bar)
    {
    	$this->bar = $bar;
    }
    
    public function baz(): int
    {
    	return ($this->bar)(1);
    }
}
Psalm output (using commit efa9b13):

ERROR: UndefinedDocblockClass - 9:1 - Docblock-defined class or interface T does not exist

INFO: MissingClosureParamType - 9:9 - Parameter $t has no provided type

INFO: MissingClosureReturnType - 9:6 - Closure does not have a return type, expecting mixed

ERROR: InvalidArgument - 11:4 - Argument 1 expects T, 1 provided

ERROR: UndefinedDocblockClass - 17:13 - Docblock-defined class or interface A does not exist

ERROR: InvalidPropertyAssignmentValue - 27:19 - $this->bar with declared type 'callable(A):A' cannot be assigned type 'callable(A:fn-foo::__construct as mixed):A:fn-foo::__construct as mixed'

ERROR: InvalidArgument - 32:26 - Argument 1 expects A, 1 provided

ERROR: InvalidReturnStatement - 32:13 - The inferred type 'A' does not match the declared return type 'int' for Foo::baz

ERROR: InvalidReturnType - 30:28 - The declared return type 'int' for Foo::baz is incorrect, got 'A'

@weirdan
Copy link
Collaborator

weirdan commented Mar 24, 2021

I don't think the example you provided could be made sound. If you instantiate Foo with callable(string):string, baz() will certainly fail.

@muglug
Copy link
Collaborator

muglug commented Mar 24, 2021

Hack doesn't support templated closures, and I don't think there's much value in Psalm supporting them either. Opens up a whole can of worms.

@muglug muglug closed this as completed Mar 24, 2021
@AndrolGenhald
Copy link
Collaborator

@template can only be used on classes and functions.
This shows the issue @weirdan pointed out more clearly: https://psalm.dev/r/1d705e3bee

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/1d705e3bee
<?php

/**
 * @template T
 */
class Foo
{
    /**
     * @var callable(T): T
     */
    private $bar;
    
    /**
     * @param callable(T): T $bar
     */
    public function __construct(callable $bar)
    {
    	$this->bar = $bar;
    }
    
    public function baz(): int
    {
    	return ($this->bar)(1);
    }
}
Psalm output (using commit efa9b13):

ERROR: InvalidArgument - 23:26 - Argument 1 expects T:Foo as mixed, 1 provided

INFO: MixedReturnTypeCoercion - 23:13 - The type 'T:Foo as mixed' is more general than the declared return type 'int' for Foo::baz

INFO: MixedReturnTypeCoercion - 21:28 - The declared return type 'int' for Foo::baz is more specific than the inferred return type 'T:Foo as mixed'

@marcosh
Copy link
Contributor Author

marcosh commented Mar 24, 2021

Thanks for your replies.

My snippets actually raised two issues:

  • passing a polymorphic function as a parameter, which you discussed in your answers. My example makes sense if you see $bar as a function from A to A for any possible type A. These are what are usually called existential types. For example in Haskell you could use them as follows
data Poly = Poly (forall a. a -> a)

The only value with type forall a. a -> a is the identity function.

  • the second issue is typing anonymous functions. As per the documentation
/**
 * @template T
 * @psalm-param T $t
 * @return T
 */
function mirror($t) {
    return $t;
}

works. Still, if I define the same function as an anonymous functions

/**
 * @template T
 * @var callable(T): T $f
 * @param T $t
 * @return T
 */
$f = fn($t) => $t;

the template T is not recognised correctly

@weirdan
Copy link
Collaborator

weirdan commented Mar 24, 2021

For the latter, you need docblock to immediately precede the closure: https://psalm.dev/r/0f93b31944. Note, however, that Psalm does not support templated closures as @muglug noted above.

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/0f93b31944
<?php
$_f = 
/**
 * @template T
 * @param T $t
 * @return T
 */
 fn($t) => $t;
Psalm output (using commit 2edf613):

No issues!

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

4 participants