Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/collection/api/collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,19 +333,19 @@ groupBy(Closure $keySelector, ?Closure $valueTransform = null): Map<K, Collectio
Group elements by key selector `(E, int): K`. When a `$valueTransform` `(E, int): V` is provided, each element is transformed before being added to its group — the result is `Map<K, ImmutableList<V>>`.

```php
intersect(iterable $other): ImmutableSet<E>
intersect(iterable<V> $other): ImmutableSet<E&V>
```
Elements present in both this collection and the iterable. Returns a set (duplicates removed).
Elements present in both this collection and the iterable. Returns a set (duplicates removed). The element type narrows to `E&V` — only values that can belong to both sides.

```php
union(iterable $other): ImmutableSet<E>
union(iterable<NE> $other): ImmutableSet<E|NE>
```
All elements from both this collection and the iterable. Returns a set (duplicates removed).
All elements from both this collection and the iterable. Returns a set (duplicates removed). The element type widens to `E|NE`, like `add()`.

```php
subtract(iterable $other): ImmutableSet<E>
subtract(iterable<mixed> $other): ImmutableSet<E>
```
Elements present in this collection but not in the iterable. Returns a set (duplicates removed).
Elements present in this collection but not in the iterable. Returns a set (duplicates removed). The element type is always `E` — any iterable may be subtracted.

## Ordering

Expand Down
12 changes: 7 additions & 5 deletions src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -582,25 +582,27 @@ public function groupBy(Closure $keySelector, ?Closure $valueTransform = null):
/**
* Returns a set containing only elements present in both this collection and the given iterable.
*
* @param iterable<E> $other
* @return Set<E>
* @template V
* @param iterable<V> $other
* @return Set<E&V>
*/
#[NoDiscard]
public function intersect(iterable $other): Set;

/**
* Returns a set containing all elements from both this collection and the given iterable.
*
* @param iterable<E> $other
* @return Set<E>
* @template NE
* @param iterable<NE> $other
* @return Set<E|NE>
*/
#[NoDiscard]
public function union(iterable $other): Set;

/**
* Returns a set containing elements present in this collection but not in the given iterable.
*
* @param iterable<E> $other
* @param iterable<mixed> $other
* @return Set<E>
*/
#[NoDiscard]
Expand Down
9 changes: 7 additions & 2 deletions src/CollectionLogic.php
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,9 @@ public function groupBy(Closure $keySelector, ?Closure $valueTransform = null):
/**
* {@inheritDoc}
*
* @return ImmutableSet<E>
* @template U
* @param iterable<U> $other
* @return ImmutableSet<E&U>
*/
#[NoDiscard]
public function intersect(iterable $other): ImmutableSet
Expand All @@ -968,7 +970,9 @@ public function intersect(iterable $other): ImmutableSet
/**
* {@inheritDoc}
*
* @return ImmutableSet<E>
* @template NE
* @param iterable<NE> $other
* @return ImmutableSet<E|NE>
*/
#[NoDiscard]
public function union(iterable $other): ImmutableSet
Expand All @@ -979,6 +983,7 @@ public function union(iterable $other): ImmutableSet
/**
* {@inheritDoc}
*
* @param iterable<mixed> $other
* @return ImmutableSet<E>
*/
#[NoDiscard]
Expand Down
15 changes: 9 additions & 6 deletions src/Operation/SetOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
final class SetOperation extends AbstractOperation
{
/**
* @param iterable<V> $other
* @return Generator<V>
* @template U
* @param iterable<U> $other
* @return Generator<V&U>
*/
public function intersect(iterable $other): Generator
{
Expand All @@ -35,14 +36,16 @@ public function intersect(iterable $other): Generator
$hash = KeyHasher::hashSetKey($v);
if (isset($otherSet[$hash]) && !isset($seen[$hash])) {
$seen[$hash] = true;
yield $v;
// Hash membership in $otherSet means the value also occurs in $other.
yield $v; // @phpstan-ignore generator.valueType
}
}
}

/**
* @param iterable<V> $other
* @return Generator<V>
* @template U
* @param iterable<U> $other
* @return Generator<V|U>
*/
public function union(iterable $other): Generator
{
Expand All @@ -65,7 +68,7 @@ public function union(iterable $other): Generator
}

/**
* @param iterable<V> $other
* @param iterable<mixed> $other
* @return Generator<V>
*/
public function subtract(iterable $other): Generator
Expand Down
9 changes: 6 additions & 3 deletions src/Set/SelfPreservingImmutableSetLogic.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
* `newCollectionOf()` builds derived collections with `new static(...)`, so there is
* no factory method to override.
*
* Two consequences of the `static` promise, both intentional:
* Three consequences of the `static` promise, all intentional:
* - Mutations are **strict**: unlike the widening base `add(NE): ImmutableSet<E|NE>`,
* here `add(E): static`. A fixed-type collection cannot widen its element type.
* - `union()` is **strict** for the same reason: unlike the widening base
* `union(iterable<NE>): Set<E|NE>`, here `union(iterable<E>): static`.
* - Type-changing methods (map, flatMap, flatten, filterInstanceOf, groupBy, the
* to* conversions) are **not** narrowed — they still return the base type, because
* their result is no longer a collection of `E`. (`groupBy` additionally cannot be
Expand Down Expand Up @@ -424,7 +426,8 @@ public function shuffled(): static
/**
* {@inheritDoc}
*
* @param iterable<E> $other
* @template V
* @param iterable<V> $other
* @return static
*/
#[NoDiscard]
Expand All @@ -448,7 +451,7 @@ public function union(iterable $other): static
/**
* {@inheritDoc}
*
* @param iterable<E> $other
* @param iterable<mixed> $other
* @return static
*/
#[NoDiscard]
Expand Down
9 changes: 7 additions & 2 deletions src/Set/SetLogic.php
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,9 @@ public function groupBy(Closure $keySelector, ?Closure $valueTransform = null):
* Routed through newCollectionOf (rather than the free setOf) so that subtypes
* which override the factory preserve their own type. Defaults to ImmutableSet.
*
* @return ImmutableSet<E>
* @template U
* @param iterable<U> $other
* @return ImmutableSet<E&U>
*/
#[NoDiscard]
public function intersect(iterable $other): ImmutableSet
Expand All @@ -387,7 +389,9 @@ public function intersect(iterable $other): ImmutableSet
/**
* {@inheritDoc}
*
* @return ImmutableSet<E>
* @template NE
* @param iterable<NE> $other
* @return ImmutableSet<E|NE>
*/
#[NoDiscard]
public function union(iterable $other): ImmutableSet
Expand All @@ -398,6 +402,7 @@ public function union(iterable $other): ImmutableSet
/**
* {@inheritDoc}
*
* @param iterable<mixed> $other
* @return ImmutableSet<E>
*/
#[NoDiscard]
Expand Down
51 changes: 51 additions & 0 deletions tests/Type/SetOperationsTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/**
* This file is part of the Noctud Collection.
* Copyright (c) Noctud.dev
*/

declare(strict_types=1);

namespace Noctud\Collection\Tests\Type;

use Noctud\Collection\Collection;
use function Noctud\Collection\listOf;
use function Noctud\Collection\setOf;
use function PHPStan\Testing\assertType;

/** @var Collection<string> $c */
$c = listOf(['a', 'b', 'c']);
/** @var Collection<int|string> $scalars */
$scalars = listOf([1, 'a']);
/** @var iterable<int> $ints */
$ints = [1, 2];
/** @var iterable<mixed> $anything */
$anything = [1, 'a'];

// With a same-typed iterable, the element type is unchanged.
assertType('Noctud\Collection\Set\Set<string>', $c->intersect(['a', 'b']));
assertType('Noctud\Collection\Set\Set<string>', $c->union(['a', 'b']));
assertType('Noctud\Collection\Set\Set<string>', $c->subtract(['a', 'b']));

// intersect narrows to E&V: only values that can belong to both sides.
assertType('Noctud\Collection\Set\Set<int>', $scalars->intersect($ints));
assertType('Noctud\Collection\Set\Set<string>', $c->intersect($anything));
// Intersecting disjoint types is reported as an unresolvable return type.
assertType('Noctud\Collection\Set\Set<*NEVER*>', $c->intersect($ints)); // @phpstan-ignore method.unresolvableReturnType
Comment thread
delacry marked this conversation as resolved.

// union widens to E|V: elements of both sides end up in the result.
assertType('Noctud\Collection\Set\Set<int|string>', $c->union($ints));
assertType('Noctud\Collection\Set\Set<int|string>', $scalars->union($ints));

// subtract always keeps E: any iterable may be subtracted.
assertType('Noctud\Collection\Set\Set<int|string>', $scalars->subtract($ints));
assertType('Noctud\Collection\Set\Set<string>', $c->subtract($anything));

// Same behavior on a Set receiver.
$imm = setOf([1, 2, 3]);
/** @var iterable<string> $strings */
$strings = ['a', 'b'];
assertType('Noctud\Collection\Set\Set<*NEVER*>', $imm->intersect($strings)); // @phpstan-ignore method.unresolvableReturnType
assertType('Noctud\Collection\Set\Set<int|string>', $imm->union($strings));
assertType('Noctud\Collection\Set\Set<int>', $imm->subtract($strings));
Loading