From 6b5efa103e9d3803c7d167521fbe7ce68c4eb63f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Fri, 6 Nov 2020 13:13:34 +0100 Subject: [PATCH] Introduce immutability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/ArrayInterface.php | 8 +--- src/Array_.php | 21 +--------- src/GenericMap.php | 1 + src/GenericOrderedList.php | 1 + src/Map.php | 77 +++++++++++++++++++++++++----------- src/MapInterface.php | 3 -- src/OrderedList.php | 52 +++++++++++++----------- src/OrderedListInterface.php | 4 +- 8 files changed, 89 insertions(+), 78 deletions(-) diff --git a/src/ArrayInterface.php b/src/ArrayInterface.php index bc8d62d..04f5682 100644 --- a/src/ArrayInterface.php +++ b/src/ArrayInterface.php @@ -14,37 +14,31 @@ * @template TKey of array-key * @template TValue * @template-extends IteratorAggregate + * @psalm-immutable */ interface ArrayInterface extends IteratorAggregate, Countable { /** * @psalm-param TValue $element - * @psalm-mutation-free */ public function contains($element): bool; /** * @psalm-return TValue - * @psalm-mutation-free * @throws OutOfBoundsException if there are no values available. */ public function first(); /** * @psalm-return TValue - * @psalm-mutation-free * @throws OutOfBoundsException if there are no values available. */ public function last(); - /** - * @psalm-mutation-free - */ public function isEmpty(): bool; /** * @psalm-return array - * @psalm-mutation-free */ public function toNativeArray(): array; } diff --git a/src/Array_.php b/src/Array_.php index 08c76bf..9751294 100644 --- a/src/Array_.php +++ b/src/Array_.php @@ -22,6 +22,7 @@ * @template TKey of array-key * @template TValue * @template-implements ArrayInterface + * @psalm-immutable */ abstract class Array_ implements ArrayInterface { @@ -31,7 +32,7 @@ abstract class Array_ implements ArrayInterface /** * @psalm-param array $data */ - public function __construct(array $data) + protected function __construct(array $data) { $this->data = $data; } @@ -44,17 +45,11 @@ public function getIterator(): Traversable return new ArrayIterator($this->data); } - /** - * @psalm-mutation-free - */ public function contains($element): bool { return in_array($element, $this->data, true); } - /** - * @psalm-mutation-free - */ public function first() { if ($this->isEmpty()) { @@ -64,9 +59,6 @@ public function first() return reset($this->data); } - /** - * @psalm-mutation-free - */ public function last() { if ($this->isEmpty()) { @@ -76,25 +68,16 @@ public function last() return end($this->data); } - /** - * @psalm-mutation-free - */ public function isEmpty(): bool { return $this->count() === 0; } - /** - * @psalm-mutation-free - */ public function count(): int { return count($this->data); } - /** - * @psalm-mutation-free - */ public function toNativeArray(): array { return $this->data; diff --git a/src/GenericMap.php b/src/GenericMap.php index 5ed7fe6..70bf722 100644 --- a/src/GenericMap.php +++ b/src/GenericMap.php @@ -8,6 +8,7 @@ * @template TKey of string * @template TValue * @template-extends Map + * @psalm-immutable */ final class GenericMap extends Map { diff --git a/src/GenericOrderedList.php b/src/GenericOrderedList.php index a54f34b..920fa54 100644 --- a/src/GenericOrderedList.php +++ b/src/GenericOrderedList.php @@ -7,6 +7,7 @@ /** * @template TValue * @template-extends OrderedList + * @psalm-immutable */ final class GenericOrderedList extends OrderedList { diff --git a/src/Map.php b/src/Map.php index 88733ed..c5001d6 100644 --- a/src/Map.php +++ b/src/Map.php @@ -31,9 +31,18 @@ * @template TValue * @template-extends Array_ * @template-implements MapInterface + * @psalm-immutable */ abstract class Map extends Array_ implements MapInterface { + /** + * @psalm-param array $data + */ + public function __construct(array $data = []) + { + parent::__construct($data); + } + public function merge(...$stack): MapInterface { $instance = clone $this; @@ -46,8 +55,8 @@ public function merge(...$stack): MapInterface public function sort(?callable $callback = null): MapInterface { - $data = $this->data; $instance = clone $this; + $data = $instance->data; if ($callback === null) { asort($data, SORT_NATURAL); $instance->data = $data; @@ -55,6 +64,7 @@ public function sort(?callable $callback = null): MapInterface return $instance; } + /** @psalm-suppress ImpureFunctionCall */ uasort($data, $callback); $instance->data = $data; @@ -63,13 +73,20 @@ public function sort(?callable $callback = null): MapInterface public function diffKeys(MapInterface $other, ?callable $keyComparator = null): MapInterface { - $instance = clone $this; - $otherData = $other->toNativeArray(); + $instance = clone $this; + $otherData = $other->toNativeArray(); + $keyComparator = $keyComparator ?? $this->keyComparator(); - /** @psalm-var array $diff1 */ - $diff1 = array_diff_ukey($this->data, $otherData, $keyComparator ?? $this->keyComparator()); - /** @psalm-var array $diff2 */ - $diff2 = array_diff_ukey($otherData, $this->data, $keyComparator ?? $this->keyComparator()); + /** + * @psalm-var array $diff1 + * @psalm-suppress ImpureFunctionCall + */ + $diff1 = array_diff_ukey($instance->data, $otherData, $keyComparator); + /** + * @psalm-var array $diff2 + * @psalm-suppress ImpureFunctionCall + */ + $diff2 = array_diff_ukey($otherData, $instance->data, $keyComparator); $merged = array_merge( $diff1, $diff2 @@ -97,6 +114,8 @@ public function toOrderedList(?callable $sorter = null): OrderedListInterface } $data = $this->data; + + /** @psalm-suppress ImpureFunctionCall */ usort($data, $sorter); return new GenericOrderedList($data); @@ -104,15 +123,13 @@ public function toOrderedList(?callable $sorter = null): OrderedListInterface public function filter(callable $callback): MapInterface { - $instance = clone $this; - $instance->data = array_filter($this->data, $callback, ARRAY_FILTER_USE_BOTH); + $instance = clone $this; + /** @psalm-suppress ImpureFunctionCall */ + $instance->data = array_filter($instance->data, $callback, ARRAY_FILTER_USE_BOTH); return $instance; } - /** - * @psalm-mutation-free - */ public function keys(): OrderedListInterface { $keys = array_keys($this->data); @@ -128,9 +145,6 @@ public function put($key, $value): MapInterface return $instance; } - /** - * @psalm-mutation-free - */ public function get(string $key) { if (! $this->has($key)) { @@ -157,7 +171,10 @@ public function intersect(MapInterface $other, ?callable $valueComparator = null private function intersection(MapInterface $other, ?callable $valueComparator, ?callable $keyComparator): array { if ($valueComparator && $keyComparator) { - /** @psalm-var array $intersection */ + /** + * @psalm-var array $intersection + * @psalm-suppress ImpureFunctionCall + */ $intersection = array_uintersect_uassoc( $this->data, $other->toNativeArray(), @@ -169,7 +186,10 @@ private function intersection(MapInterface $other, ?callable $valueComparator, ? } if ($keyComparator) { - /** @psalm-var array $intersection */ + /** + * @psalm-var array $intersection + * @psalm-suppress ImpureFunctionCall + */ $intersection = array_intersect_ukey($this->data, $other->toNativeArray(), $keyComparator); return $intersection; @@ -179,7 +199,10 @@ private function intersection(MapInterface $other, ?callable $valueComparator, ? $valueComparator = $this->valueComparator(); } - /** @psalm-var array $intersection */ + /** + * @psalm-var array $intersection + * @psalm-suppress ImpureFunctionCall + */ $intersection = array_uintersect($this->data, $other->toNativeArray(), $valueComparator); return $intersection; @@ -214,14 +237,20 @@ public function intersectUserAssoc( public function diff(MapInterface $other, ?callable $valueComparator = null): MapInterface { - /** @psalm-var array $diff1 */ + /** + * @psalm-var array $diff1 + * @psalm-suppress ImpureFunctionCall + */ $diff1 = array_udiff( $this->toNativeArray(), $other->toNativeArray(), $valueComparator ?? $this->valueComparator() ); - /** @psalm-var array $diff2 */ + /** + * @psalm-var array $diff2 + * @psalm-suppress ImpureFunctionCall + */ $diff2 = array_udiff( $other->toNativeArray(), $this->toNativeArray(), @@ -263,12 +292,12 @@ public function removeElement($element): MapInterface public function map(callable $callback): MapInterface { - return new GenericMap(array_map($callback, $this->data)); + $instance = clone $this; + + /** @psalm-suppress ImpureFunctionCall */ + return new GenericMap(array_map($callback, $instance->data)); } - /** - * @psalm-mutation-free - */ public function has(string $key): bool { return array_key_exists($key, $this->data); diff --git a/src/MapInterface.php b/src/MapInterface.php index ec1cfad..cddb053 100644 --- a/src/MapInterface.php +++ b/src/MapInterface.php @@ -81,7 +81,6 @@ public function unset($key): MapInterface; /** * @psalm-return OrderedListInterface - * @psalm-mutation-free */ public function keys(): OrderedListInterface; @@ -96,7 +95,6 @@ public function put($key, $value): MapInterface; * @psalm-param TKey $key * @psalm-return TValue * @throws OutOfBoundsException if key does not exist. - * @psalm-mutation-free */ public function get(string $key); @@ -128,7 +126,6 @@ public function intersectUserAssoc( /** * @psalm-param TKey $key - * @psalm-mutation-free */ public function has(string $key): bool; diff --git a/src/OrderedList.php b/src/OrderedList.php index 83dafaf..71526af 100644 --- a/src/OrderedList.php +++ b/src/OrderedList.php @@ -33,13 +33,14 @@ * @template TValue * @template-extends Array_ * @template-implements OrderedListInterface + * @psalm-immutable */ abstract class OrderedList extends Array_ implements OrderedListInterface { /** * @psalm-param list $data */ - final public function __construct(array $data) + final public function __construct(array $data = []) { parent::__construct($data); } @@ -51,13 +52,14 @@ public function merge(...$stack): OrderedListInterface return $list->toNativeArray(); }, $stack); - $instance->data = array_values(array_merge($this->data, ...$values)); + $instance->data = array_values(array_merge($instance->data, ...$values)); return $instance; } public function map(callable $callback): OrderedListInterface { + /** @psalm-suppress ImpureFunctionCall */ return new GenericOrderedList(array_values( array_map($callback, $this->data) )); @@ -71,9 +73,6 @@ public function add($element): OrderedListInterface return $instance; } - /** - * @psalm-mutation-free - */ public function at(int $position) { if (! array_key_exists($position, $this->data)) { @@ -85,8 +84,8 @@ public function at(int $position) public function sort(?callable $callback = null): OrderedListInterface { - $data = $this->data; $instance = clone $this; + $data = $instance->data; if ($callback === null) { sort($data, SORT_NATURAL); $instance->data = $data; @@ -94,6 +93,7 @@ public function sort(?callable $callback = null): OrderedListInterface return $instance; } + /** @psalm-suppress ImpureFunctionCall */ usort($data, $callback); $instance->data = $data; @@ -102,18 +102,24 @@ public function sort(?callable $callback = null): OrderedListInterface public function diff(OrderedListInterface $other, ?callable $valueComparator = null): OrderedListInterface { + $instance = clone $this; + + $valueComparator = $valueComparator ?? $this->valueComparator(); + + /** @psalm-suppress ImpureFunctionCall */ $diff1 = array_udiff( - $this->toNativeArray(), + $instance->toNativeArray(), $other->toNativeArray(), - $valueComparator ?? $this->valueComparator() + $valueComparator ); + + /** @psalm-suppress ImpureFunctionCall */ $diff2 = array_udiff( $other->toNativeArray(), - $this->toNativeArray(), - $valueComparator ?? $this->valueComparator() + $instance->toNativeArray(), + $valueComparator ); - $instance = clone $this; $instance->data = array_values(array_merge( $diff1, $diff2 @@ -124,7 +130,8 @@ public function diff(OrderedListInterface $other, ?callable $valueComparator = n public function intersect(OrderedListInterface $other, ?callable $valueComparator = null): OrderedListInterface { - $instance = clone $this; + $instance = clone $this; + /** @psalm-suppress ImpureFunctionCall */ $instance->data = array_values(array_uintersect( $instance->data, $other->toNativeArray(), @@ -141,7 +148,9 @@ public function intersect(OrderedListInterface $other, ?callable $valueComparato */ public function toMap(callable $keyGenerator): MapInterface { - $keys = array_map($keyGenerator, $this->data); + $instance = clone $this; + /** @psalm-suppress ImpureFunctionCall */ + $keys = array_map($keyGenerator, $instance->data); Assert::allStringNotEmpty($keys); $combined = array_combine( @@ -166,7 +175,7 @@ public function removeElement($element): OrderedListInterface { /** @psalm-suppress MissingClosureParamType */ return $this->filter( - static function ($value) use ($element): bool { + static function (/** @param TValue $value */ $value) use ($element): bool { return $value !== $element; } ); @@ -174,9 +183,10 @@ static function ($value) use ($element): bool { public function filter(callable $callback): OrderedListInterface { - $instance = clone $this; + $instance = clone $this; + /** @psalm-suppress ImpureFunctionCall */ $instance->data = array_values( - array_filter($this->data, $callback) + array_filter($instance->data, $callback) ); return $instance; @@ -203,6 +213,7 @@ public function unify( foreach ($instance->data as $value) { $identifier = $unificationIdentifierGenerator($value); try { + /** @psalm-suppress ImpureMethodCall */ $unique = $unified->get($identifier); } catch (OutOfBoundsException $exception) { $unique = $value; @@ -212,9 +223,11 @@ public function unify( $unique = $callback($unique, $value); } + /** @psalm-suppress ImpureMethodCall */ $unified = $unified->put($identifier, $unique); } + /** @psalm-suppress ImpureMethodCall */ $instance->data = $unified->toOrderedList()->toNativeArray(); return $instance; @@ -249,6 +262,7 @@ public function fill(int $startIndex, int $amount, $value): OrderedListInterface */ private function createListFilledWithValues(int $start, int $amount, $value): array { + /** @psalm-suppress ImpureFunctionCall */ if (! is_callable($value)) { /** @psalm-var array $list */ $list = array_fill($start, $amount, $value); @@ -264,9 +278,6 @@ private function createListFilledWithValues(int $start, int $amount, $value): ar return $list; } - /** - * @psalm-mutation-free - */ public function slice(int $offset, ?int $length = null): OrderedListInterface { $instance = clone $this; @@ -275,9 +286,6 @@ public function slice(int $offset, ?int $length = null): OrderedListInterface return $instance; } - /** - * @psalm-mutation-free - */ public function find(callable $callback) { foreach ($this->data as $value) { diff --git a/src/OrderedListInterface.php b/src/OrderedListInterface.php index c66b470..09a1f70 100644 --- a/src/OrderedListInterface.php +++ b/src/OrderedListInterface.php @@ -10,6 +10,7 @@ /** * @template TValue * @template-extends ArrayInterface + * @psalm-immutable */ interface OrderedListInterface extends ArrayInterface { @@ -22,7 +23,6 @@ public function add($element): OrderedListInterface; /** * @psalm-return TValue * @throws OutOfBoundsException If position does not exist. - * @psalm-mutation-free */ public function at(int $position); @@ -98,14 +98,12 @@ public function fill(int $startIndex, int $amount, $value): OrderedListInterface /** * @psalm-return OrderedListInterface - * @psalm-mutation-free */ public function slice(int $offset, ?int $length = null): OrderedListInterface; /** * @psalm-param Closure(TValue):bool $callback * @psalm-return TValue - * @psalm-mutation-free * @throws OutOfBoundsException if value could not be found with provided callback. */ public function find(callable $callback);