Skip to content

Commit

Permalink
TypedMap
Browse files Browse the repository at this point in the history
  • Loading branch information
vudaltsov committed Sep 4, 2024
1 parent ac3f83a commit 3419ff1
Show file tree
Hide file tree
Showing 4 changed files with 328 additions and 0 deletions.
144 changes: 144 additions & 0 deletions src/MutableTypedMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php

declare(strict_types=1);

namespace Typhoon\DataStructures;

/**
* @api
*/
final class MutableTypedMap extends TypedMap
{
/**
* @param array<non-negative-int, mixed> $values
*/
private function __construct(
private array $values = [],
) {}

public static function create(TypedKVPair ...$kvPairs): self
{
$map = new self();

foreach ($kvPairs as $pair) {
$map->values[$pair->key->index] = $pair->value;
}

return $map;
}

public function with(TypedKey $key, mixed $value): static
{
$values = $this->values;
$values[$key->index] = $value;

return new self($values);
}

public function withAll(self $map): static
{
return new self(array_replace($this->values, $map->values));
}

public function without(TypedKey ...$keys): static
{
$values = $this->values;

foreach ($keys as $key) {
unset($values[$key->index]);
}

return new self($values);
}

public function contains(TypedKey $key): bool
{
return \array_key_exists($key->index, $this->values);
}

public function offsetExists(mixed $offset): bool
{
return \array_key_exists($offset->index, $this->values);
}

/**
* @template V
* @param TypedKey<V> $key
* @return V
*/
public function get(TypedKey $key): mixed
{
if (\array_key_exists($key->index, $this->values)) {
/** @var V */
return $this->values[$key->index];
}

return $key->default($this);
}

/**
* @template V
* @param TypedKey<V> $offset
* @return V
*/
public function offsetGet(mixed $offset): mixed
{
if (\array_key_exists($offset->index, $this->values)) {
/** @var V */
return $this->values[$offset->index];
}

return $offset->default($this);
}

public function count(): int
{
return \count($this->values);
}

/**
* @template V
* @param TypedKey<V> $key
* @param V $value
*/
public function put(TypedKey $key, mixed $value): void
{
$this->values[$key->index] = $value;
}

public function putAll(self $map): void
{
$this->values = array_replace($this->values, $map->values);
}

public function remove(TypedKey ...$keys): void
{
foreach ($keys as $key) {
unset($this->values[$key->index]);
}
}

/**
* @return list<array{class-string<TypedKey>, non-empty-string, mixed}>
*/
public function __serialize(): array
{
return array_map(
static fn(TypedKey $key, mixed $value): array => [$key::class, $key->method, $value],
TypedKey::byIndexes(array_keys($this->values)),
$this->values,
);
}

/**
* @param list<array{class-string<TypedKey>, non-empty-string, mixed}> $data
*/
public function __unserialize(array $data): void
{
foreach ($data as [$class, $method, $value]) {
$key = $class::$method();
\assert($key instanceof $class);
$this->values[$key->index] = $value;
}
}
}
21 changes: 21 additions & 0 deletions src/TypedKVPair.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Typhoon\DataStructures;

/**
* @api
* @template V
*/
final class TypedKVPair
{
/**
* @param TypedKey<V> $key
* @param V $value
*/
public function __construct(
public readonly TypedKey $key,
public readonly mixed $value,
) {}
}
101 changes: 101 additions & 0 deletions src/TypedKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

namespace Typhoon\DataStructures;

/**
* @api
* @template V
* @psalm-consistent-templates
*/
abstract class TypedKey
{
/**
* @var array<non-empty-string, self>
*/
private static array $keys = [];

/**
* @internal
* @psalm-internal Typhoon\DataStructures
* @param list<non-negative-int> $indexes
* @return list<self>
*/
final public static function byIndexes(array $indexes): array
{
$keys = array_values(self::$keys);

return array_map(
static fn(int $index): self => $keys[$index] ?? throw new \LogicException(),
$indexes,
);
}

/**
* @template D
* @param ?callable(TypedMap): D $default
* @return static<D>
*/
final protected static function init(?callable $default = null): static
{
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1] ?? [];

\assert(
isset($trace['class'], $trace['function']) && $trace['class'] === static::class && $trace['function'] !== '',
\sprintf('Invalid %s call', self::class),
);

$name = \sprintf('%s::%s', static::class, $trace['function']);

\assert(!isset(self::$keys[$name]), \sprintf('Please ensure you memoize key in %s()', $name));

return self::$keys[$name] = new static(
index: \count(self::$keys),
method: $trace['function'],
default: $default ?? static fn(): never => throw new \LogicException(\sprintf('Key %s() does not have a default value', $name)),
);
}

/**
* @param non-negative-int $index
* @param non-empty-string $method
* @param callable(TypedMap): V $default
*/
final private function __construct(
public readonly int $index,
public readonly string $method,
private readonly mixed $default,
) {}

/**
* @return V
*/
final public function default(TypedMap $map): mixed
{
return ($this->default)($map);
}

/**
* @return non-empty-string
*/
final public function toString(): string
{
return self::class . '::' . $this->method . '()';
}

final public function __serialize(): never
{
throw new \BadMethodCallException(\sprintf('%s does not support serialization', self::class));
}

final public function __unserialize(array $_data): never
{
throw new \BadMethodCallException(\sprintf('%s does not support deserialization', self::class));
}

final public function __clone()
{
throw new \BadMethodCallException(\sprintf('%s does not support cloning', self::class));
}
}
62 changes: 62 additions & 0 deletions src/TypedMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace Typhoon\DataStructures;

/**
* @api
* @implements \ArrayAccess<TypedKey, mixed>
* @psalm-suppress UnusedClass
*/
abstract class TypedMap implements \ArrayAccess, \Countable
{
public static function create(TypedKVPair ...$kvPairs): self
{
return MutableTypedMap::create(...$kvPairs);
}

/**
* @template V
* @param TypedKey<V> $key
* @param V $value
*/
abstract public function with(TypedKey $key, mixed $value): static;

abstract public function withAll(MutableTypedMap $map): static;

abstract public function without(TypedKey ...$keys): static;

abstract public function contains(TypedKey $key): bool;

abstract public function offsetExists(mixed $offset): bool;

/**
* @template V
* @param TypedKey<V> $key
* @return V
*/
abstract public function get(TypedKey $key): mixed;

/**
* @template V
* @param TypedKey<V> $offset
* @return V
*/
abstract public function offsetGet(mixed $offset): mixed;

/**
* @return non-negative-int
*/
abstract public function count(): int;

public function offsetSet(mixed $offset, mixed $value): never
{
throw new \BadMethodCallException();
}

public function offsetUnset(mixed $offset): never
{
throw new \BadMethodCallException();
}
}

0 comments on commit 3419ff1

Please sign in to comment.