Skip to content

Instantly share code, notes, and snippets.

@jesseschalken
Last active May 3, 2017 03:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jesseschalken/babe293ef421da3a7d71fa3e35325002 to your computer and use it in GitHub Desktop.
Save jesseschalken/babe293ef421da3a7d71fa3e35325002 to your computer and use it in GitHub Desktop.
<?hh // strict
namespace JesseSchalken\HackTypes;
class TypeError<T> extends \Exception {
public function __construct(private mixed $x, private Type<T> $type) {
parent::__construct();
}
}
function typeof(mixed $x): string {
if (is_int($x)) {
return 'int';
} else if (is_string($x)) {
return 'string';
} else if (is_null($x)) {
return 'void';
} else if (is_float($x)) {
return 'float';
} else if (is_object($x)) {
return get_class($x);
} else if (is_bool($x)) {
return 'bool';
} else if (is_resource($x)) {
return 'resource';
} else if (is_array($x)) {
$keys = [];
$vals = [];
$assoc = false;
$i = 0;
foreach ($x as $k => $v) {
$assoc = $assoc || $k !== $i++;
$keys[typeof($k)] = true;
$vals[typeof($v)] = true;
}
if ($assoc) {
return 'array<'.merge_types($keys).', '.merge_types($vals).'>';
} else {
return 'array<'.merge_types($vals).'>';
}
} else {
throw new \Exception();
}
}
function merge_types(array<string, mixed> $types): string {
$nullable = false;
if (array_key_exists('void', $types)) {
$nullable = true;
unset($types['void']);
}
switch (count($types)) {
case 0:
$type = '_';
break;
case 1:
$type = array_keys($types)[0];
break;
case 2:
if (array_key_exists('int', $types) ||
array_key_exists('float', $types)) {
$type = 'num';
} else if (array_key_exists('int', $types) ||
array_key_exists('string', $types)) {
$type = 'arraykey';
} else {
$type = 'mixed';
}
break;
default:
$type = 'mixed';
break;
}
if ($nullable) {
$type = '?'.$type;
}
return $type;
}
abstract class CastResult<+T> {
public abstract function assert(): T;
public abstract function isOk(): bool;
public abstract function orDefault(): T;
}
final class CastOk<T> extends CastResult<T> {
public function __construct(private T $value) {}
public function assert(): T {
return $this->value;
}
public function isOk(): bool {
return true;
}
public function orDefault(): T {
return $this->value;
}
}
final class CastFail<T> extends CastResult<T> {
public function __construct(private Type<T> $type) {}
public function isOk(): bool {
return false;
}
public function assert(): T {
throw new \Exception();
}
public function orDefault(): T {
return $this->type->getDefault();
}
}
abstract class Type<T> {
public abstract function cast(mixed $x): CastResult<T>;
public abstract function getDefault(): T;
public abstract function toString(): string;
public final function unNull(?T $x): T {
return $x === null ? $this->getDefault() : $x;
}
public function castOk(T $v): CastResult<T> {
return new CastOk($v);
}
public function castFail(): CastResult<T> {
return new CastFail($this);
}
}
class TypeString extends Type<string> {
public function getDefault(): string {
return '';
}
public function cast(mixed $x): CastResult<string> {
return is_string($x) ? $this->castOk($x) : $this->castFail();
}
public function toString(): string {
return 'string';
}
}
class TypeBool extends Type<bool> {
public function getDefault(): bool {
return false;
}
public function cast(mixed $x): CastResult<bool> {
return is_bool($x) ? $this->castOk($x) : $this->castFail();
}
public function toString(): string {
return 'bool';
}
}
class TypeFloat extends Type<float> {
public function getDefault(): float {
return 0.0;
}
public function cast(mixed $x): CastResult<float> {
return is_float($x) ? $this->castOk($x) : $this->castFail();
}
public function toString(): string {
return 'float';
}
}
class TypeInt extends Type<int> {
public function getDefault(): int {
return 0;
}
public function cast(mixed $x): CastResult<int> {
return is_int($x) ? $this->castOk($x) : $this->castFail();
}
public function toString(): string {
return 'int';
}
}
class TypeObject<T> extends Type<T> {
public function __construct(private classname<T> $class) {}
public function cast(mixed $x): CastResult<T> {
return $x instanceof $this->class ? $this->castOk($x) : $this->castFail();
}
public function toString(): string {
return $this->class;
}
public function getDefault(): T {
throw new \Exception('nope');
}
}
class TypeListArray<T> extends Type<array<T>> {
public function __construct(private Type<T> $type) {}
public function getDefault(): array<T> {
return [];
}
public function cast(mixed $x): CastResult<array<T>> {
if (is_array($x)) {
$i = 0;
foreach ($x as $k => $v) {
if ($k !== $i++ || !$this->type->cast($v)->isOk()) {
return $this->castFail();
}
}
return $this->castOk($x);
}
return $this->castFail();
}
public function toString(): string {
return 'array<'.$this->type->toString().'>';
}
}
class TypeMapArray<Tk, Tv> extends Type<array<Tk, Tv>> {
public function __construct(private Type<Tk> $key, private Type<Tv> $val) {}
public function getDefault(): array<Tk, Tv> {
return [];
}
public function cast(mixed $x): CastResult<array<Tk, Tv>> {
if (is_array($x)) {
foreach ($x as $k => $v) {
if (!$this->key->cast($k)->isOk()) {
return $this->castFail();
}
if (!$this->val->cast($v)->isOk()) {
return $this->castFail();
}
}
return $this->castOk($x);
}
return $this->castFail();
}
public function toString(): string {
return 'array<'.$this->key->toString().', '.$this->val->toString().'>';
}
}
class TypeArrayKey extends Type<arraykey> {
public function getDefault(): arraykey {
return '';
}
public function cast(mixed $x): CastResult<arraykey> {
if (is_string($x)) {
return $this->castOk($x);
}
if (is_int($x)) {
return $this->castOk($x);
}
return $this->castFail();
}
public function toString(): string {
return 'arraykey';
}
}
class TypeNum extends Type<num> {
public function getDefault(): num {
return 0;
}
public function cast(mixed $x): CastResult<num> {
if (is_int($x)) {
return $this->castOk($x);
}
if (is_float($x)) {
return $this->castOk($x);
}
return $this->castFail();
}
public function toString(): string {
return 'num';
}
}
class TypeMixed extends Type<mixed> {
public function getDefault(): mixed {
return null;
}
public function cast(mixed $x): CastResult<mixed> {
return $this->castOk($x);
}
public function toString(): string {
return 'mixed';
}
}
class TypeNullable<T> extends Type<?T> {
public function __construct(private Type<T> $type) {}
public function getDefault(): ?T {
return null;
}
public function cast(mixed $x): CastResult<?T> {
return $x === null ? $this->castOk(null) : $this->type->cast($x);
}
public function toString(): string {
return '?'.$this->type->toString();
}
}
class TypeResource extends Type<resource> {
public function getDefault(): resource {
throw new \Exception();
}
public function cast(mixed $x): CastResult<resource> {
return is_resource($x) ? $this->castOk($x) : $this->castFail();
}
public function toString(): string {
return 'resource';
}
}
class Foo {
public function blah(): void {}
}
function assert_object<T>(mixed $x, classname<T> $class): T {
if ($x instanceof $class) {
return $x;
}
throw new \Exception();
}
function test(mixed $y): void {
$y = new TypeListArray(
new TypeMapArray(
new TypeString(),
new TypeNullable(new TypeObject(Foo::class)),
),
)->cast($y)->assert();
foreach ($y as $k => $v) {
needs_int($k);
foreach ($v as $k2 => $v2) {
needs_string($k2);
if ($v2 !== null) {
$v2->blah();
}
}
}
}
function needs_int(int $x): void {}
function needs_string(string $x): void {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment