psalm
psalm copied to clipboard
Nested callables + templates -> something went wrong
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
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
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 It's like Promise:all()
from JavaScript.
@AndrolGenhald It seems it is fixed by now!