On Github HackPack / presenting-hack
By Brian Scaturro / @scaturr
The type checker is your best line of defense. However, the runtime will allow things the type checker does not approve of.
The type checker is invoked by using the hh_client executable that installs with Hack. This executable is currently only available on *nix platforms.
hh_client will search for types based on the location of a .hhconfig file. It is an empty file. Be sure to include this in the root of your projects.
<?hh //strict function sum(int x, int y): int { return x + y; }
<?hh //strict class TestResult { protected int $runCount; protected array<Failure> $failures; }
<?hh //strict function invoke((function(int $x): int) $fn): int { return $fn(1); }
<?hh //strict class FluentObject { public function doThing(): this { //do something return $this; } }
https://github.com/HackPack/HackUnit/blob/bug-broken-this-annotation/Runner/Options.php
<?hh //strict namespace HackPack\HackUnit\Runner; class Options { protected string $testPath; /** * Use Options type since "this" annotation is broken * when namespaces are used */ public function setTestPath(string $testPath): Options { $this->testPath; return $this; } }
The Hack type checker has some pretty interesting ways of handling the "mixed" type that many PHP programmers are familiar with. The type checker will give you a pass on it if it sees you are validating the type.
<?hh function sum(mixed $x): void { if (is_array($x) || $x instanceof Vector) { $s = 0; foreach ($x as $v) { $s += $v; } return $s; } //... do something else or throw an exception... }
http://docs.hhvm.com/manual/en/hack.annotations.mixedtypes.php
PHP 5.5 introduced some features that programmers in other languages have been enjoying for years: generators and coroutines. Hack has given types to both of these.
<?hh //strict function infiniteIterator(): Continuation<int> { $i = 0; while (true) { yield ++$i; } }
<?hh async function f(): Awaitable<int> { return 42; } async function g(): Awaitable<string> { $f = await f(); $f++; return 'hi test ' . $f; }
http://docs.hhvm.com/manual/en/hack.annotations.generators.php
Hack has various levels of tolerance for it's type checker. These are known as modes, and they are triggered by comments.
For the pure of heart. Everything must be annotated and everything is type checked. Strict code cannot call into non Hack code.
<?hh //strict class TestCase { public function __construct(protected string $name) { } public function setUp(): void { } public function expect<T>(T $context): Expectation<T> { return new Expectation($context); } }
It is worth noting that top level code cannot exist in strict mode. This makes it impossible to execute a purely strict program (there is no "main" method). Partial or UNSAFE must be used as strict entry points.
Strict mode will throw "Unbound name" errors when it can't find a corresponding type (including functions). Out of the box, this also includes native PHP functions! You can imagine how frustrating this would be everytime you reach for your favorite PHP function.
hhi files contain interfaces for most of the PHP core library. These files basically provide type information to Hack's type checker. You only have to make sure they are on the path of the type checker. After install, these are found at /usr/share/hhvm/Hack/hhi. Just copy them to your project's directory and you should be good.
Since strict mode does not allow top level code, partial mode is the method for program entry.
https://github.com/HackPack/HackUnit/blob/master/bin/Hackunit
https://github.com/HackPack/HackUnit/blob/master/Runner/Loading/StandardLoader.php#L98
The benefit of course being: generics can be statically checked.
<?hh //strict namespace HackPack\Hacktions; trait EventEmitter { protected Map<string, Vector<(function(...): void)>> $listeners = Map {}; }
The type checker can easily catch these sort of type constraints, and our program design is better for them.
While Hack generics are useful type annotations, they are nowhere near as useful as generics in Java/C#.
This mainly stems from Hack's preference for inference and the fact that a type is not a concrete thing in Hack.
The preference for inference seems like a blow to readability. The following results in a type error:
$fun = () ==> { $fn = $this->callable; $fn(); } $this->expectCallable($fun)->toThrow<ExpectationException>(); //Tests/Core/CallableExpectationTest.php|54 col 70 error| This operator is not associative, add parentheses
$fun = () ==> { $fn = $this->callable; $fn();}; $this->expectCallable($fun)->toThrow('\HackPack\HackUnit\Core\ExpectationException');
This snippet also demonstrates we haven't left the "magic string" pattern of PHP. Types are not concrete things in Hack - we still have to check types against strings.
Generics are not as useful in Hack because they don't stack. What I mean by stack is explained by the following from the docs:
A generic method must not collide with any existing, non-generic method name (i.e, public function swap and public function swap).<?hh //strict class Cook { use Subject<Waiter>; use Subject<Busboy>; } //throws type errors
Generics are really useful design tools. Their presence in Hack is a welcome addition that is not present in vanilla PHP. However, they are not as useful as they are in other languages.
<?hh //strict class Options { protected ?string $HackUnitFile; public function getHackUnitFile(): ?string { $path = (string) getcwd() . '/Hackunit.php'; if (! is_null($this->HackUnitFile)) { $path = $this->HackUnitFile; } $path = realpath($path); return $path ?: null; } }
The following results in a type error:
<?hh //strict class TestResult { protected ?float $startTime; public function getTime(): ?float { $time = null; $startTime = $this->startTime; $time = microtime(true) - $startTime; return $time; } //TestResult.php|39 col 35 error| Typing error //TestResult.php|39 col 35 error| This is a num (int/float) because this is used in an arithmetic operation //TestResult.php|13 col 15 error| It is incompatible with a nullable type }
<?hh //strict class TestResult { public function getTime(): ?float { $time = null; $startTime = $this->startTime; if (!is_null($startTime)) { $time = microtime(true) - $startTime; } return $time; } }
Nullable allows you to be explicit about the possibility of null. This makes code more readable and easier to reason about.
The goals of Hack collections are four-fold:
Provide a unified collections framework that is simple and intuitive. Provide equal or better performance than the equivalent PHP array pattern. Provide a collection implementation that allows for optional static typing, integrating seamlessly with Hack. Provide an easy migration path to this framework by building on top of standard functionality from PHP5.https://github.com/HackPack/Hacktions/blob/master/Subject.php
https://github.com/HackPack/Hacktions/blob/master/EventEmitter.php
Note: Maps only support integer and string keys for now.
Note: Sets only support integer and string values for now.
Most Hack collections have immutable variants - i.e ImmVector. Immutable variants function like their mutable counterparts with the exception that items cannot be added or removed.
Collections are now the preferred method of storing things. Arrays are still allowed, but they must be used in a new way to conform to the type checker.
https://github.com/HackPack/HackUnit/blob/master/Error/TraceParser.php
<?hh //strict type Origin = shape( 'method' => string, 'message' => string, 'location' => string );
<?hh //strict newtype Location = shape( 'file' => string, 'line' => int );
Opaque type aliases work like their non-opaque counterpart with the exception that they cannot escape the confines of the file they were defined in. http://bit.ly/1mDUWwj
<?hh newtype closedfile = resource; newtype openfile = resource; function get_file_handler(string $filename): closedfile { return some_wrapped_function($filename); } function open_file_handler(closedfile $file): openfile { $file->open(); return $file; } function read(openfile $file): string { return $file->read(); }
Async is not threading. It is cooperative multitasking. Dash those ideas of easily running things in parallel.
While not threading, this is still extremely useful, and it will only get better. See the official example on coalesced fetching.
Lambdas make event driven code gorgeous.
$ui = new Text(); $this->runner->on('testFailed', (...) ==> $ui->printFeedback("\033[41;37mF\033[0m")); $this->runner->on('testPassed', (...) ==> $ui->printFeedback('.'));
https://github.com/HackPack/HackUnit/blob/master/UI/Console.php
$squared = array_map($x ==> $x*$x, array(1,2,3));
This may seem like a small addition, but it is probably one of the coolest features of Hack.
$ui = new Text(); $this->runner->on('testFailed', (...) ==> $ui->printFeedback("\033[41;37mF\033[0m")); //vs $this->runner->>on('testFailed', function(...) use ($ui) { $ui->printFeedback("\033[41;37mF\033[0m";) });
function returns_tuple(): (int, string, bool) { return tuple(1, "two", true); } class UsesTuples { protected Vector<(int, string)> $vectorOfIntAndStringTuples; }
<?hh //strict class TestResult { public function __construct(protected int $runCount = 0, protected int $errorCount = 0) { } }
Hack has some other rules and features, but here are some of the more relevant ones.
http://docs.hhvm.com/manual/en/Hack.otherrulesandfeatures.varargs.php
http://docs.hhvm.com/manual/en/Hack.otherrulesandfeatures.callbacks.php
There are a few variants of fun:
$factory = class_meth('\HackPack\HackUnit\Runner\Loading\StandardLoader', 'create'); $update = inst_meth($observer, 'update');
https://github.com/HackPack/HackUnit/blob/master/UI/Console.php https://github.com/HackPack/Hacktions/blob/master/Subject.php
http://docs.hhvm.com/manual/en/Hack.otherrulesandfeatures.invariant.php
public function notifyObservers(...): void { foreach ($this->observers as $observer) { invariant($observer instanceof Observer, 'Subjects can only notify Observers'); $update = inst_meth($observer, 'update'); $args = array_merge([$this], func_get_args()); call_user_func_array($update, $args); } }
https://github.com/HackPack/Hacktions/blob/master/Subject.php