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

Keep track of templates for callables #4589

Open
thomasvargiu opened this issue Nov 17, 2020 · 10 comments
Open

Keep track of templates for callables #4589

thomasvargiu opened this issue Nov 17, 2020 · 10 comments

Comments

@thomasvargiu
Copy link
Contributor

Consider the following two examples.
I think it should be possible to keep track of template references and infer the right type of the return type.

https://psalm.dev/r/6e44e1eccf
https://psalm.dev/r/4efcc7e309

/**
 * @template B
 * @psalm-return callable(B): B
 */
function foo(): callable
{
    return function ($fab) {
        return $fab;
    };
}

For example, in the above code, as the interal psalm code does, function foo() should return a callable(B:fn-foo as mixed): B:fn-foo as mixed, so that calling this callable we know that the return type is the first argument.

Same in the following code:

/**
 * @template T
 * @template B
 * @psalm-param T $e
 * @psalm-return callable(callable(T): B): B
 */
function foo($e): callable
{
    return function ($fab) use ($e) {
        return $fab($e);
    };
}

Here, nested callables should keep track of the template.

Without this improvement, it's almost impossible to work with nested callables, specially developing or documenting a functional programming library.

I think it should be possible, right?

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/6e44e1eccf
<?php

/**
 * @template T
 * @template B
 * @psalm-param T $e
 * @psalm-return callable(callable(T): B): B
 */
function foo($e): callable
{
    return function ($fab) use ($e) {
        return $fab($e);
    };
}


foo('foo')(fn (string $a) => strlen($a));
Psalm output (using commit fda2377):

ERROR: InvalidScalarArgument - 17:12 - Argument 1 expects callable(string(foo)):empty, pure-Closure(string):int provided
https://psalm.dev/r/4efcc7e309
<?php

/**
 * @template B
 * @psalm-return callable(B): B
 */
function foo(): callable
{
    return function ($fab) {
        return $fab;
    };
}


foo()('bar');
Psalm output (using commit fda2377):

ERROR: InvalidScalarArgument - 15:7 - Argument 1 expects empty, string(bar) provided

@thomasvargiu
Copy link
Contributor Author

thomasvargiu commented Nov 26, 2020

Looking at this example:
https://psalm.dev/r/b6c1411ec4

I think it should be possible.

  1. fist call foo('foo') should return callable(string(foo)): B:fn-foo
  2. B-foo should continue to be a templated type because is recognized as a deferred type, because it's a result of a parameter (the callable)
  3. second call should accepts a callable(string(foo)): B:fn-foo and infer return type from the caller
  4. same logic should be recursive

@weirdan what do you think?

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/b6c1411ec4
<?php

/**
 * @template T
 * @template B
 * @psalm-param T $e
 * @psalm-return callable(callable(T): B): B
 */
function foo($e): callable
{
    return function ($fab) use ($e) {
        return $fab($e);
    };
}


foo('foo')(fn (string $a) => strlen($a));
Psalm output (using commit 74c07bb):

ERROR: InvalidScalarArgument - 17:12 - Argument 1 expects callable(string(foo)):empty, pure-Closure(string):int provided

@weirdan
Copy link
Collaborator

weirdan commented Nov 27, 2020

That would mean foo should return a generic callable (something like callable<T of mixed>(callable(string(foo)): T): T) and to my knowledge Psalm does not support generic callables at the moment.
Technically this does typecheck and run (https://psalm.dev/r/b618884527, https://3v4l.org/EHf6N) when rewritten using a class, so the question is all about the docblock syntax.

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/b618884527
<?php

/** 
 * @template T
 */
class myInvokable {
    /** @var T */
    private $e;
    
    /** @param T $e */
    public function __construct($e) { $this->e = $e; }
    
    /**
     * @template B
     * @param callable(T): B $p
     * @return B
     */
    public function __invoke($p) {
        return $p($this->e);
    }
}

/**
 * @template T
 * @psalm-param T $e
 * @return myInvokable<T>
 */
function foo($e): callable
{
    return new myInvokable($e);
}

$_z = foo('foo')(fn (string $a) => strlen($a));
/** @psalm-trace $_z */;
// var_dump($_z);
Psalm output (using commit 74c07bb):

INFO: Trace - 34:24 - $_z: int

@thomasvargiu
Copy link
Contributor Author

In your example you are wrapping almost the same logic in a invokable class, but with the same behaviour. But it's impossible with already existing librararies, and excessive to creare a class for a simple lambda function, and I think it's something that will be use more in the next years starting from PHP 7.4 with arrow functions.
I was trying to do it writing a plugin (FunctionReturnTypeProviderInterface) for a pipe() function (composition of one or more callables, pipe(f1, f2, f3)($a)), returning a chain of callables keeping the "generics, and then evaluating calls with a AfterStatementAnalysisInterface`, revaluating variables in context. Obviously I was just trying and playing with it, but with more knolodgment I think it would be possible to make a plugin for it (at least for a specific function).

Now, a function like that will use empty and not even mixed, so it's a little bit complicated to use it writing the right return type.

@weirdan
Copy link
Collaborator

weirdan commented Nov 27, 2020

In your example you are wrapping almost the same logic in a invokable class, but with the same behaviour. But it's impossible with already existing librararies

Indeed. The point of this exercise was to see how it maps to concepts already used in Psalm and to check if it's sound. I'm not suggesting you to actually rewrite anything.

@thomasvargiu
Copy link
Contributor Author

Oh, maybe as the first thing, should psalm support templates in closures? Or I'm doing something wrong here?
https://psalm.dev/r/79c407cbf4

That's why my experimental plugin wasn't working :)

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/79c407cbf4
<?php

$f1 =
    /**
     * @template T
     * @param T $e
     * @return T
     */
    function ($e)
    {
        return $e;
    };

$valueF1 = $f1('a');
Psalm output (using commit b717356):

ERROR: InvalidArgument - 14:16 - Argument 1 expects T:fn-/var/www/vhosts/psalm.dev/httpdocs/src/somefile.php:9:88:-:closure as mixed, string(a) provided

@thomasvargiu
Copy link
Contributor Author

Probably related to #4550

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

No branches or pull requests

3 participants