Last active
May 3, 2017 03:08
-
-
Save jesseschalken/babe293ef421da3a7d71fa3e35325002 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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