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

Template generics inside callables #4485

Closed
thomasvargiu opened this issue Nov 5, 2020 · 8 comments
Closed

Template generics inside callables #4485

thomasvargiu opened this issue Nov 5, 2020 · 8 comments

Comments

@thomasvargiu
Copy link
Contributor

Hi, I'm trying to implements templates, but psalm infer return type as callable(empty): empty and I don't understand why, is it a bug?

https://psalm.dev/r/eafedd80ab

@psalm-github-bot
Copy link

I found these snippets:

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

/**
 * @template T
 *
 * @psalm-param callable(T): mixed $func
 * @psalm-return Closure(T):T
 */
function tee(callable $func): Closure {
    /**
     * @template A
     * @psalm-param A $a
     * @psalm-return A
     */
    return function ($a) use ($func) {
        $func($a);

        return $a;
    };
}

/** @psalm-trace $f */
$f = tee(fn (int $a): string => $a . 'foo');

/** @psalm-trace $result */
$result = $f(5);
Psalm output (using commit d47d817):

INFO: Trace - 23:1 - $f: Closure(empty):empty

ERROR: InvalidScalarArgument - 26:14 - Argument 1 expects empty, int(5) provided

INFO: Trace - 26:1 - $result: empty

INFO: UnusedVariable - 26:1 - Variable $result is never referenced

@thomasvargiu
Copy link
Contributor Author

Sorry, this is the correct snippet:

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

@psalm-github-bot
Copy link

I found these snippets:

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

/**
 * @template T
 * @psalm-param callable(T): mixed $func
 * @psalm-return Closure(T): T
 */
function tee(callable $func): Closure {
    return function ($a) use ($func) {
        $func($a);

        return $a;
    };
}

/** @psalm-trace $f */
$f = tee(fn (int $a): string => $a . 'foo');

/** @psalm-trace $result */
$result = $f(5);
Psalm output (using commit d47d817):

INFO: Trace - 17:1 - $f: Closure(empty):empty

ERROR: InvalidScalarArgument - 20:14 - Argument 1 expects empty, int(5) provided

INFO: Trace - 20:1 - $result: empty

@weirdan weirdan added the bug label Nov 5, 2020
@muglug muglug removed the bug label Nov 6, 2020
@muglug
Copy link
Collaborator

muglug commented Nov 6, 2020

It's not really a bug.

You're defining a lower-bound for the param, but not an upper bound. A slightly different version of the code helps Psalm understand what's going on by providing the upper bound for T early: https://psalm.dev/r/ee47d3ca0f

In a language like Hack you could explicitly pass the template params in the call like tee<int>(fn (int $a): string => $a), setting the upper and lower bound for the value of T explicitly, but Psalm has to infer an upper bound and there's nothing for it to use.

@muglug muglug closed this as completed Nov 6, 2020
@psalm-github-bot
Copy link

I found these snippets:

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

/**
 * @template T
 * @psalm-param callable(T):mixed $func
 * @psalm-param T $a
 * @psalm-return pure-Closure(): T
 */
function tee(callable $func, $a): Closure {
    return function () use ($func, $a) {
        $func($a);

        return $a;
    };
}

function foo(int $i) : int {
    /** @psalm-trace $f */
    $f = tee(fn (int $a): string => $a . 'foo', $i);

    return $f();
}
Psalm output (using commit debedf2):

INFO: Trace - 19:5 - $f: pure-Closure():int

@thomasvargiu
Copy link
Contributor Author

@muglug I was playing with templates, and I'm wondering why the same function with a wrapped callable works, and a simple callable not.

https://psalm.dev/r/02085aa6f8

I don't really know the internals, and I don't know if it will be possible, maybe it could be an improvement, but why a templated object works and a callable not? If it works with an object, does it means that psalm can infer types correctly even for closures?

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/02085aa6f8
<?php

/**
 * @template A
 * @template B
 */
class ClosureContainer {
    /** @var callable(A): B */
    private $f;

    /**
     * @param callable(A): B $f
     */
    public function __construct(callable $f)
    {
        $this->f = $f;
    }

    /**
     * @param A $x
     * @return B
     */
    public function __invoke($x)
    {
        return ($this->f)($x);
    }
}

/**
 * @template T
 * @psalm-param callable(T): mixed $func
 * @psalm-return callable(T): T
 */
function tee(callable $func): callable {
    return
        /**
         * @psalm-param T $value
         * @psalm-return T
         */
        function ($value) use ($func) {
            $func($value);
            return $value;
        };
}

/**
 * Wrapped closure version
 *
 * @template T
 * @param ClosureContainer<T, mixed> $func
 * @return ClosureContainer<T, T>
 */
function tee2(ClosureContainer $func): ClosureContainer {
    $f =
        /**
         * @psalm-param T $value
         * @psalm-return T
         */
        function ($value) use ($func) {
            $func($value);
            return $value;
        };
    return new ClosureContainer($f);
}

$f = fn (string $a): int => strlen($a);
/** @psalm-trace $value */
$value = tee($f)('foo');


// Wrapped closure version

/** @psalm-trace $value */
$value = tee2(new ClosureContainer($f))('foo');
Psalm output (using commit 165e0db):

ERROR: InvalidScalarArgument - 68:18 - Argument 1 expects empty, string(foo) provided

INFO: Trace - 68:1 - $value: empty

INFO: Trace - 74:1 - $value: string

@muglug
Copy link
Collaborator

muglug commented Nov 11, 2020

You're right, it should, and now it does!

danog pushed a commit to danog/psalm that referenced this issue Jan 29, 2021
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

3 participants