Skip to content

Commit 6c8bac0

Browse files
committed
Add README.md
1 parent dadb29e commit 6c8bac0

File tree

1 file changed

+382
-0
lines changed

1 file changed

+382
-0
lines changed

README.md

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
# fp4php/functional psalm plugin
2+
3+
...labels...
4+
5+
## Installation
6+
7+
Supported installation method is via [composer](https://getcomposer.org):
8+
9+
```shell
10+
composer require fp4php/functional-psalm-plugin --dev
11+
```
12+
13+
## Usage
14+
15+
To enable the plugin, add the `Fp\PsalmPlugin\FunctionalPlugin` class to your psalm configuration using `psalm-plugin` binary as follows:
16+
17+
```shell
18+
php vendor/bin/psalm-plugin enable fp4php/functional-psalm-plugin
19+
```
20+
21+
## Features
22+
23+
- #### filter
24+
25+
Plugin add type narrowing for filtering.
26+
27+
`Fp\Functional\Option\Option::filter`:
28+
29+
```php
30+
<?php
31+
32+
declare(strict_types=1);
33+
34+
use Fp\Functional\Option\Option;
35+
36+
/**
37+
* @return Option<int|string>
38+
*/
39+
function getOption(): Option
40+
{
41+
// ...
42+
}
43+
44+
// Narrowed to Option<string>
45+
46+
/** @psalm-trace $result */
47+
$result = getOption()->filter(fn($value) => is_string($value));
48+
```
49+
50+
`Fp\Collections\ArrayList::filter` (and other collections with
51+
`filter` method):
52+
53+
```php
54+
<?php
55+
56+
declare(strict_types=1);
57+
58+
use Fp\Collections\ArrayList;
59+
60+
/**
61+
* @return ArrayList<int|string>
62+
*/
63+
function getArrayList(): ArrayList
64+
{
65+
// ...
66+
}
67+
68+
// Narrowed to ArrayList<string>
69+
70+
/** @psalm-trace $result */
71+
$result = getArrayList()->filter(fn($value) => is_string($value));
72+
```
73+
74+
`Fp\Functional\Either\Either::filterOrElse`:
75+
76+
```php
77+
<?php
78+
79+
declare(strict_types=1);
80+
81+
use TypeError;
82+
use ValueError;
83+
use Fp\Functional\Either\Either;
84+
85+
/**
86+
* @return Either<ValueError, int|string>
87+
*/
88+
function getEither(): Either
89+
{
90+
// ...
91+
}
92+
93+
// Narrowed to Either<TypeError|ValueError, string>
94+
getEither()->filterOrElse(
95+
fn($value) => is_string($value),
96+
fn() => new TypeError('Is not string'),
97+
);
98+
```
99+
100+
`Fp\Collection\filter`:
101+
102+
```php
103+
<?php
104+
105+
declare(strict_types=1);
106+
107+
use function Fp\Collection\filter;
108+
109+
/**
110+
* @return list<int|string>
111+
*/
112+
function getList(): array
113+
{
114+
// ...
115+
}
116+
117+
// Narrowed to list<string>
118+
filter(getList(), fn($value) => is_string($value));
119+
```
120+
121+
`Fp\Collection\first` and `Fp\Collection\last`:
122+
123+
```php
124+
<?php
125+
126+
declare(strict_types=1);
127+
128+
use function Fp\Collection\first;
129+
use function Fp\Collection\last;
130+
131+
/**
132+
* @return list<int|string>
133+
*/
134+
function getList(): array
135+
{
136+
// ...
137+
}
138+
139+
// Narrowed to Option<string>
140+
first(getList(), fn($value) => is_string($value));
141+
142+
// Narrowed to Option<int>
143+
last(getList(), fn($value) => is_int($value));
144+
```
145+
146+
For all cases above you can use [first-class
147+
callable](https://wiki.php.net/rfc/first_class_callable_syntax)
148+
syntax:
149+
150+
```php
151+
<?php
152+
153+
declare(strict_types=1);
154+
155+
use function Fp\Collection\filter;
156+
157+
/**
158+
* @return list<int|string>
159+
*/
160+
function getList(): array
161+
{
162+
// ...
163+
}
164+
165+
// Narrowed to list<string>
166+
filter(getList(), is_string(...));
167+
```
168+
169+
- #### fold
170+
171+
Is too difficult to make the fold function using type system of
172+
psalm. Without plugin `Fp\Collection\fold` and collections fold
173+
method has some edge cases. For example:
174+
<https://psalm.dev/r/b0a99c4912>
175+
176+
Plugin can fix that problem.
177+
178+
- #### ctor
179+
180+
PHP 8.1 brings feature called [first-class
181+
callable](https://wiki.php.net/rfc/first_class_callable_syntax). But
182+
that feature cannot be used for class constructor.
183+
`Fp\Callable\ctor` can simulate this feature for class constructors,
184+
but requires plugin for static analysis.
185+
186+
```php
187+
<?php
188+
189+
use Tests\Mock\Foo;
190+
191+
use function Fp\Callable\ctor;
192+
193+
// Psalm knows that ctor(Foo::class) is Closure(int, bool, bool): Foo
194+
test(ctor(Foo::class));
195+
196+
/**
197+
* @param Closure(int, bool, bool): Foo $makeFoo
198+
*/
199+
function test(Closure $makeFoo): void
200+
{
201+
print_r($makeFoo(42, true, false));
202+
print_r(PHP_EOL);
203+
}
204+
```
205+
206+
- #### sequence
207+
208+
Plugin brings structural type inference for sequence functions:
209+
210+
```php
211+
<?php
212+
213+
use Fp\Functional\Option\Option;
214+
215+
use function Fp\Collection\sequenceOption;
216+
use function Fp\Collection\sequenceOptionT;
217+
218+
function getFoo(int $id): Option
219+
{
220+
// ...
221+
}
222+
223+
function getBar(int $id): Option
224+
{
225+
// ...
226+
}
227+
228+
/**
229+
* @return Option<array{foo: Foo, bar: Bar}>
230+
*/
231+
function sequenceOptionShapeExample(int $id): Option
232+
{
233+
// Inferred type is: Option<array{foo: Foo, bar: Bar}> not Option<array<'foo'|'bar', Foo|Bar>>
234+
return sequenceOption([
235+
'foo' => getFoo($id),
236+
'bar' => getBar($id),
237+
]);
238+
}
239+
240+
/**
241+
* @return Option<array{Foo, Bar}>
242+
*/
243+
function sequenceOptionTupleExample(int $id): Option
244+
{
245+
// Inferred type is: Option<array{Foo, Bar}> not Option<list<Foo|Bar>>
246+
return sequenceOptionT(getFoo($id), getBar($id));
247+
}
248+
```
249+
250+
- #### assertion
251+
252+
Unfortunately `@psalm-assert-if-true`/`@psalm-assert-if-false` works
253+
incorrectly for Option/Either assertion methods:
254+
<https://psalm.dev/r/408248f46f>
255+
256+
Plugin implements workaround for this bug.
257+
258+
- #### N-combinators
259+
260+
Psalm plugin will prevent calling *N combinator in non-valid cases:
261+
262+
```php
263+
<?php
264+
265+
declare(strict_types=1);
266+
267+
use Fp\Functional\Option\Option;
268+
use Tests\Mock\Foo;
269+
270+
/**
271+
* @param Option<array{int, bool}> $maybeData
272+
* @return Option<Foo>
273+
*/
274+
function test(Option $maybeData): Option
275+
{
276+
/*
277+
* ERROR: IfThisIsMismatch
278+
* Object must be type of Option<array{int, bool, bool}>, actual type Option<array{int, bool}>
279+
*/
280+
return $maybeData->mapN(fn(int $a, bool $b, bool $c) => new Foo($a, $b, $c));
281+
}
282+
```
283+
284+
- #### proveTrue
285+
286+
Implementation assertion effect for `Fp\Evidence\proveTrue` (like
287+
for builtin `assert` function):
288+
289+
```php
290+
<?php
291+
292+
use Fp\Functional\Option\Option;
293+
294+
function getIntOrString(): int|string
295+
{
296+
// ...
297+
}
298+
299+
Option::do(function() {
300+
$value = getIntOrString();
301+
yield proveTrue(is_int($value));
302+
303+
// here $value narrowed to int from int|string
304+
});
305+
```
306+
307+
- #### toEither
308+
309+
Inference for `Fp\Functional\Separated\Separated::toEither`:
310+
311+
```php
312+
<?php
313+
314+
use Fp\Collections\HashSet;
315+
use Fp\Collections\ArrayList;
316+
use Fp\Functional\Either\Either;
317+
use Fp\Functional\Separated\Separated;
318+
319+
/**
320+
* @param Separated<ArrayList<int>, ArrayList<string>> $separated
321+
* @return Either<ArrayList<int>, ArrayList<string>>
322+
*/
323+
function separatedArrayListToEither(Separated $separated): Either
324+
{
325+
return $separated->toEither();
326+
}
327+
328+
/**
329+
* @param Separated<HashSet<int>, HashSet<string>> $separated
330+
* @return Either<HashSet<int>, HashSet<string>>
331+
*/
332+
function separatedHashSetToEither(Separated $separated): Either
333+
{
334+
return $separated->toEither();
335+
}
336+
```
337+
338+
- #### partitionT
339+
340+
Plugin infers each `list` type from predicates of `partitionT`:
341+
342+
```php
343+
<?php
344+
345+
declare(strict_types=1);
346+
347+
use Tests\Mock\Foo;
348+
use Tests\Mock\Bar;
349+
use Tests\Mock\Baz;
350+
351+
use function Fp\Collection\partitionT;
352+
353+
/**
354+
* @param list<Foo|Bar|Baz> $list
355+
* @return array{list<Foo>, list<Bar>, list<Baz>}
356+
*/
357+
function testExhaustiveInference(array $list): array
358+
{
359+
return partitionT($list, fn($i) => $i instanceof Foo, fn($i) => $i instanceof Bar);
360+
}
361+
```
362+
363+
- #### filterNotNull
364+
365+
Plugin turns all nullable keys to possibly undefined keys:
366+
367+
```php
368+
<?php
369+
370+
declare(strict_types=1);
371+
372+
use function Fp\Collection\filterNotNull;
373+
374+
/**
375+
* @param array{name: string, age: int|null} $shape
376+
* @return array{name: string, age?: int}
377+
*/
378+
function example(array $shape): array
379+
{
380+
return filterNotNull($shape);
381+
}
382+
```

0 commit comments

Comments
 (0)