psalm icon indicating copy to clipboard operation
psalm copied to clipboard

Nested callables + templates -> something went wrong

Open someniatko opened this issue 2 years ago • 3 comments

I can't figure out whether it's me typing Option:all() incorrectly, or it's a bug in Psalm:

https://psalm.dev/r/5b9a372af4

someniatko avatar Jul 29 '22 15:07 someniatko

I found these snippets:

https://psalm.dev/r/5b9a372af4
<?php

/**
 * @psalm-immutable
 * @template-covariant TValue
 */
abstract class Option
{
    /**
     * @template T
     * @param list<Option<T>> $options
     * @return Option<list<T>>
     */
    public static function all(array $options): Option
    {
        return array_reduce(
            $options,
            /**
             * @param Option<list<T>> $carry
             * @param Option<T> $o
             * @return Option<list<T>>
             */
            fn(Option $carry, Option $o) => $carry->flatMap(
               /**
                * @param list<T> $ts
                * @return Option<list<T>>
                */
                fn(array $ts) => $o->map(
                    /**
                     * @param T $t
                     * @return list<T>
                     */
                    fn($t) => array_merge($ts, [ $t ])
                )
            ),
            new Some([]),
        );
    }

    /**
     * @template TMap
     * @param callable(TValue):TMap $map
     * @return Option<TMap>
     */
    abstract public function map(callable $map): Option;

    /**
     * @template TMap
     * @param callable(TValue):Option<TMap> $map
     * @return Option<TMap>
     */
    abstract public function flatMap(callable $map): Option;
}

/**
 * @psalm-immutable
 * @template-covariant T
 * @template-extends Option<T>
 */
final class Some extends Option
{
    /** @var T */
    private $value;

    /** @param T $value */
    public function __construct($value)
    {
        $this->value = $value;
    }

    public function map(callable $map): Option
    {
        /** @psalm-suppress ImpureFunctionCall */
        return new Some($map($this->value));
    }

    public function flatMap(callable $map): Option
    {
        /** @psalm-suppress ImpureFunctionCall */
        return $map($this->value);
    }
}

/**
 * @psalm-immutable
 * @template-extends Option<never>
 */
final class None extends Option
{
    public function map(callable $map): Option
    {
        return $this;
    }

    public function flatMap(callable $map): Option
    {
        return $this;
    }
}
Psalm output (using commit 7c4228f):

ERROR: InvalidArgument - 33:21 - Argument 1 of Option::map expects callable(TValue:Option as mixed):mixed, pure-Closure(T:fn-option::all as mixed):list<T:fn-option::all as mixed> provided

INFO: MixedArgumentTypeCoercion - 28:17 - Argument 1 of Option::flatMap expects callable(TValue:Option as mixed):Option<mixed>, parent type pure-Closure(list<T:fn-option::all as mixed>):Option<list<T:fn-option::all as mixed>> provided

psalm-github-bot[bot] avatar Jul 29 '22 15:07 psalm-github-bot[bot]

A bit hard to tell what's going on here, but this does look like a Psalm bug.

Not really relevant, but I was confused at first because I expected all() to do this, not this.

AndrolGenhald avatar Jul 29 '22 15:07 AndrolGenhald

@AndrolGenhald It's like Promise:all() from JavaScript.

someniatko avatar Jul 29 '22 15:07 someniatko

@AndrolGenhald It seems it is fixed by now!

someniatko avatar Apr 26 '23 15:04 someniatko