diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 779f162..2f3eecb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,11 +4,11 @@ on: [push, pull_request]
jobs:
blackbox:
- uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@next
+ uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@main
coverage:
- uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@next
+ uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@main
secrets: inherit
psalm:
- uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@next
+ uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@main
cs:
- uses: innmind/github-workflows/.github/workflows/cs.yml@next
+ uses: innmind/github-workflows/.github/workflows/cs.yml@main
diff --git a/README.md b/README.md
index 3af62f3..2bd64a3 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
-# mutable
+# Mutable
-[](https://github.com/innmind/mutable/actions?query=workflow%3ACI)
+[](https://github.com/Innmind/mutable/actions/workflows/ci.yml)
[](https://codecov.io/gh/innmind/mutable)
[](https://shepherd.dev/github/innmind/mutable)
-Description
+This a collection of mutable data structures.
## Installation
@@ -14,4 +14,10 @@ composer require innmind/mutable
## Usage
-Todo
+Available structures:
+
+- `Innmind\Mutable\Map`
+- `Innmind\Mutable\Queue` FIFO queue
+- `Innmind\Mutable\Ring` Circle through a fixed sequence of data in an infinite loop
+- `Innmind\Mutable\Set`
+- `Innmind\Mutable\Stack` LIFO queue
diff --git a/blackbox.php b/blackbox.php
index ff9d705..2588bdc 100644
--- a/blackbox.php
+++ b/blackbox.php
@@ -11,7 +11,7 @@
Application::new($argv)
->when(
- \get_env('ENABLE_COVERAGE') !== false,
+ \getenv('ENABLE_COVERAGE') !== false,
static fn(Application $app) => $app
->scenariiPerProof(1)
->codeCoverage(
diff --git a/composer.json b/composer.json
index d797ac8..96ccac4 100644
--- a/composer.json
+++ b/composer.json
@@ -15,7 +15,8 @@
"issues": "http://github.com/innmind/mutable/issues"
},
"require": {
- "php": "~8.4"
+ "php": "~8.4",
+ "innmind/immutable": "~6.0"
},
"autoload": {
"psr-4": {
@@ -23,7 +24,7 @@
}
},
"require-dev": {
- "innmind/static-analysis": "^1.2.1",
+ "innmind/static-analysis": "~1.3",
"innmind/black-box": "~6.5",
"innmind/coding-standard": "~2.0"
}
diff --git a/proofs/.gitkeep b/proofs/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/proofs/queue.php b/proofs/queue.php
new file mode 100644
index 0000000..973c1c3
--- /dev/null
+++ b/proofs/queue.php
@@ -0,0 +1,43 @@
+same(0, $queue->size());
+ $assert->true($queue->empty());
+
+ foreach ($values as $i => $value) {
+ $queue->push($value);
+
+ $assert->false($queue->empty());
+ $assert->same($i + 1, $queue->size());
+ }
+
+ $pulled = [];
+ $size = $queue->size();
+
+ foreach ($values as $i => $_) {
+ $pulled[] = $queue->pull()->match(
+ static fn($value) => $value,
+ static fn() => null,
+ );
+
+ $assert->same($size - 1, $queue->size());
+ $size = $queue->size();
+ }
+
+ $assert->true($queue->empty());
+ $assert->same($values, $pulled);
+ },
+ );
+};
diff --git a/proofs/ring.php b/proofs/ring.php
new file mode 100644
index 0000000..0329669
--- /dev/null
+++ b/proofs/ring.php
@@ -0,0 +1,79 @@
+atLeast(1),
+ Set::integers()->between(1, 10),
+ ),
+ static function($assert, $values, $rotations) {
+ $ring = Ring::of(...$values);
+ $pulled = [];
+
+ foreach (\range(1, $rotations) as $_) {
+ foreach (\range(1, \count($values)) as $__) {
+ $pulled[] = $ring->pull()->match(
+ static fn($value) => $value,
+ static fn() => null,
+ );
+ }
+ }
+
+ $expected = \array_merge(
+ ...\array_fill(
+ 0,
+ $rotations,
+ $values,
+ ),
+ );
+
+ $assert->same($expected, $pulled);
+ },
+ );
+
+ yield proof(
+ 'Partial Ring rotations',
+ given(
+ Set::sequence(Set::type())->atLeast(1),
+ Set::integers()->between(1, 1_000),
+ ),
+ static function($assert, $values, $toPull) {
+ $ring = Ring::of(...$values);
+ $pulled = [];
+
+ foreach (\range(1, $toPull) as $_) {
+ $pulled[] = $ring->pull()->match(
+ static fn($value) => $value,
+ static fn() => null,
+ );
+ }
+
+ $expected = \array_slice(
+ \array_merge(
+ ...\array_fill(
+ 0,
+ (int) \ceil($toPull / \count($values)),
+ $values,
+ ),
+ ),
+ 0,
+ $toPull,
+ );
+
+ $assert->same($expected, $pulled);
+ },
+ );
+
+ yield test(
+ 'Empty Ring returns nothing',
+ static fn($assert) => $assert->false(Ring::of()->pull()->match(
+ static fn() => true,
+ static fn() => false,
+ )),
+ );
+};
diff --git a/proofs/stack.php b/proofs/stack.php
new file mode 100644
index 0000000..2843c85
--- /dev/null
+++ b/proofs/stack.php
@@ -0,0 +1,43 @@
+same(0, $stack->size());
+ $assert->true($stack->empty());
+
+ foreach ($values as $i => $value) {
+ $stack->push($value);
+
+ $assert->false($stack->empty());
+ $assert->same($i + 1, $stack->size());
+ }
+
+ $pulled = [];
+ $size = $stack->size();
+
+ foreach ($values as $i => $_) {
+ $pulled[] = $stack->pull()->match(
+ static fn($value) => $value,
+ static fn() => null,
+ );
+
+ $assert->same($size - 1, $stack->size());
+ $size = $stack->size();
+ }
+
+ $assert->true($stack->empty());
+ $assert->same(\array_reverse($values), $pulled);
+ },
+ );
+};
diff --git a/psalm.xml b/psalm.xml
index 5d768ff..a270b9b 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -14,4 +14,7 @@
+
+
+
diff --git a/src/.gitkeep b/src/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/Map.php b/src/Map.php
new file mode 100644
index 0000000..70df0a1
--- /dev/null
+++ b/src/Map.php
@@ -0,0 +1,159 @@
+ $map
+ */
+ private function __construct(
+ private Immutable\Map $map,
+ ) {
+ }
+
+ /**
+ * Set a new key/value pair
+ *
+ * @param K $key
+ * @param V $value
+ */
+ public function __invoke(mixed $key, mixed $value): void
+ {
+ $this->map = ($this->map)($key, $value);
+ }
+
+ /**
+ * @template A
+ * @template B
+ * @no-named-arguments
+ * @psalm-pure
+ *
+ * @param list $pairs
+ *
+ * @return self
+ */
+ public static function of(array ...$pairs): self
+ {
+ return new self(Immutable\Map::of(...$pairs));
+ }
+
+ /**
+ * @return int<0, max>
+ */
+ #[\NoDiscard]
+ public function size(): int
+ {
+ return $this->map->size();
+ }
+
+ /**
+ * Set a new key/value pair
+ *
+ * @param K $key
+ * @param V $value
+ */
+ public function put(mixed $key, mixed $value): void
+ {
+ $this->map = ($this->map)($key, $value);
+ }
+
+ /**
+ * Return the element with the given key
+ *
+ * @param K $key
+ *
+ * @return Maybe
+ */
+ #[\NoDiscard]
+ public function get(mixed $key): Maybe
+ {
+ return $this->map->get($key);
+ }
+
+ /**
+ * Check if there is an element for the given key
+ *
+ * @param K $key
+ */
+ #[\NoDiscard]
+ public function contains(mixed $key): bool
+ {
+ return $this->map->contains($key);
+ }
+
+ /**
+ * Remove all elements from the map
+ */
+ public function clear(): void
+ {
+ $this->map = $this->map->clear();
+ }
+
+ /**
+ * Remove all elements that don't match the predicate
+ *
+ * @param callable(K, V): bool $predicate
+ */
+ public function filter(callable $predicate): void
+ {
+ $this->map = $this->map->filter($predicate);
+ }
+
+ /**
+ * Remove all elements that match the predicate
+ *
+ * @param callable(K, V): bool $predicate
+ */
+ public function exclude(callable $predicate): void
+ {
+ $this->map = $this->map->exclude($predicate);
+ }
+
+ /**
+ * Run the given function for each element of the map
+ *
+ * @param callable(K, V): void $function
+ */
+ public function foreach(callable $function): void
+ {
+ $_ = $this->map->foreach($function);
+ }
+
+ /**
+ * Remove the element with the given key
+ *
+ * @param K $key
+ */
+ public function remove(mixed $key): void
+ {
+ $this->map = $this->map->remove($key);
+ }
+
+ #[\NoDiscard]
+ public function empty(): bool
+ {
+ return $this->map->empty();
+ }
+
+ /**
+ * @return Immutable\Map
+ */
+ #[\NoDiscard]
+ public function snapshot(): Immutable\Map
+ {
+ return $this->map;
+ }
+}
diff --git a/src/Queue.php b/src/Queue.php
new file mode 100644
index 0000000..9a1df4a
--- /dev/null
+++ b/src/Queue.php
@@ -0,0 +1,82 @@
+ $data
+ */
+ private function __construct(
+ private Sequence $data,
+ ) {
+ }
+
+ /**
+ * @template A
+ * @no-named-arguments
+ * @psalm-pure
+ *
+ * @param A ...$values
+ *
+ * @return self
+ */
+ public static function of(mixed ...$values): self
+ {
+ return new self(Sequence::of(...$values));
+ }
+
+ /**
+ * @param T $element
+ */
+ public function push(mixed $element): void
+ {
+ $this->data = ($this->data)($element);
+ }
+
+ /**
+ * @return Maybe
+ */
+ #[\NoDiscard]
+ public function pull(): Maybe
+ {
+ $value = $this->data->first();
+ $this->data = $this->data->drop(1);
+
+ return $value;
+ }
+
+ /**
+ * @return int<0, max>
+ */
+ #[\NoDiscard]
+ public function size(): int
+ {
+ return $this->data->size();
+ }
+
+ #[\NoDiscard]
+ public function empty(): bool
+ {
+ return $this->data->empty();
+ }
+
+ /**
+ * Remove all elements from the queue
+ */
+ public function clear(): void
+ {
+ $this->data = $this->data->clear();
+ }
+}
diff --git a/src/Ring.php b/src/Ring.php
new file mode 100644
index 0000000..a909b44
--- /dev/null
+++ b/src/Ring.php
@@ -0,0 +1,64 @@
+ $data
+ */
+ private function __construct(
+ private array $data,
+ ) {
+ }
+
+ /**
+ * @template A
+ * @no-named-arguments
+ * @psalm-pure
+ *
+ * @param A ...$values
+ *
+ * @return self
+ */
+ public static function of(mixed ...$values): self
+ {
+ return new self($values);
+ }
+
+ /**
+ * @return Maybe
+ */
+ public function pull(): Maybe
+ {
+ if (!\array_key_exists(0, $this->data)) {
+ /** @var Maybe */
+ return Maybe::nothing();
+ }
+
+ $value = Maybe::just(\current($this->data));
+ \next($this->data);
+
+ if (\is_null(\key($this->data))) {
+ \reset($this->data);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Move the ring cursor to the first value
+ */
+ public function reset(): void
+ {
+ \reset($this->data);
+ }
+}
diff --git a/src/Set.php b/src/Set.php
new file mode 100644
index 0000000..7f2cd49
--- /dev/null
+++ b/src/Set.php
@@ -0,0 +1,139 @@
+ $set
+ */
+ private function __construct(
+ private Immutable\Set $set,
+ ) {
+ }
+
+ /**
+ * Add an element to the set
+ *
+ * @param T $element
+ */
+ public function __invoke(mixed $element): void
+ {
+ $this->set = ($this->set)($element);
+ }
+
+ /**
+ * @template A
+ * @no-named-arguments
+ * @psalm-pure
+ *
+ * @param A $values
+ *
+ * @return self
+ */
+ public static function of(mixed ...$values): self
+ {
+ return new self(Immutable\Set::of(...$values));
+ }
+
+ /**
+ * @return int<0, max>
+ */
+ #[\NoDiscard]
+ public function size(): int
+ {
+ return $this->set->size();
+ }
+
+ /**
+ * Add an element to the set
+ *
+ * @param T $element
+ */
+ public function add(mixed $element): void
+ {
+ $this->set = ($this->set)($element);
+ }
+
+ /**
+ * Check if the set contains the given element
+ *
+ * @param T $element
+ */
+ #[\NoDiscard]
+ public function contains(mixed $element): bool
+ {
+ return $this->set->contains($element);
+ }
+
+ /**
+ * Remove the element from the set
+ *
+ * @param T $element
+ */
+ public function remove(mixed $element): void
+ {
+ $this->set = $this->set->remove($element);
+ }
+
+ /**
+ * Remove all elements that don't satisfy the given predicate
+ *
+ * @param callable(T): bool $predicate
+ */
+ public function filter(callable $predicate): void
+ {
+ $this->set = $this->set->filter($predicate);
+ }
+
+ /**
+ * Remove all elements that satisfy the given predicate
+ *
+ * @param callable(T): bool $predicate
+ */
+ public function exclude(callable $predicate): void
+ {
+ $this->set = $this->set->exclude($predicate);
+ }
+
+ /**
+ * Apply the given function to all elements of the set
+ *
+ * @param callable(T): void $function
+ */
+ public function foreach(callable $function): void
+ {
+ $_ = $this->set->foreach($function);
+ }
+
+ /**
+ * Removes all elements from the set
+ */
+ public function clear(): void
+ {
+ $this->set = $this->set->clear();
+ }
+
+ #[\NoDiscard]
+ public function empty(): bool
+ {
+ return $this->set->empty();
+ }
+
+ /**
+ * @return Immutable\Set
+ */
+ #[\NoDiscard]
+ public function snapshot(): Immutable\Set
+ {
+ return $this->set;
+ }
+}
diff --git a/src/Stack.php b/src/Stack.php
new file mode 100644
index 0000000..f86c75d
--- /dev/null
+++ b/src/Stack.php
@@ -0,0 +1,82 @@
+ $data
+ */
+ private function __construct(
+ private Sequence $data,
+ ) {
+ }
+
+ /**
+ * @template A
+ * @no-named-arguments
+ * @psalm-pure
+ *
+ * @param A ...$values
+ *
+ * @return self
+ */
+ public static function of(mixed ...$values): self
+ {
+ return new self(Sequence::of(...$values));
+ }
+
+ /**
+ * @param T $element
+ */
+ public function push(mixed $element): void
+ {
+ $this->data = ($this->data)($element);
+ }
+
+ /**
+ * @return Maybe
+ */
+ #[\NoDiscard]
+ public function pull(): Maybe
+ {
+ $value = $this->data->last();
+ $this->data = $this->data->dropEnd(1);
+
+ return $value;
+ }
+
+ /**
+ * @return int<0, max>
+ */
+ #[\NoDiscard]
+ public function size(): int
+ {
+ return $this->data->size();
+ }
+
+ #[\NoDiscard]
+ public function empty(): bool
+ {
+ return $this->data->empty();
+ }
+
+ /**
+ * Remove all elements from the queue
+ */
+ public function clear(): void
+ {
+ $this->data = $this->data->clear();
+ }
+}