Getting Started: Starting A Real Project
Starting A Real Project
Real projects generally aren't a single file in isolation; they tend to have dependencies such as the Hack Standard Library, and various optional tools.
A good starting point is to:
- install Composer
- create an empty
.hhconfig
file - create
src/
andtests/
subdirectories - configure autoloading
- use Composer to install the common dependencies and tools
Autoloading
In HHVM, there is no 'build' step as such; each file is processed as needed.
Currently, HHVM needs to be given a map of what files define which classes,
functions and so on - for example, to execute the code new Foo()
, HHVM needs
to know that the class Foo
is defined in src/Foo.hack
.
hhvm-autoload generates this map, using an hh_autoload.json
configuration
file. For most projects, a minimal example is:
{
"roots": [
"src/"
],
"devRoots": [
"tests/"
]
}
Once this configuration file is created, vendor/bin/hh-autoload
can be executed
to generate or update the map, which is created as vendor/autoload.hack
An Example
$ touch .hhconfig
$ mkdir bin src tests
$ cat > hh_autoload.json
{
"roots": [
"src/"
],
"devRoots": [
"tests/"
]
}
$ composer require hhvm/hsl hhvm/hhvm-autoload
Using version ^4.0 for hhvm/hsl
Using version ^2.0 for hhvm/hhvm-autoload
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
- Installing hhvm/hsl (v4.0.0): Loading from cache
- Installing hhvm/hhvm-autoload (v2.0.3): Loading from cache
Writing lock file
Generating autoload files
/var/folders/3l/2yk1tgkn7xdd76bs547d9j90fcbt87/T/tmp.xaQwE1xE/vendor/autoload.hack
$ composer require --dev hhvm/hhast hhvm/hacktest facebook/fbexpect
Using version ^4.0 for hhvm/hhast
Using version ^1.4 for hhvm/hacktest
Using version ^2.5 for facebook/fbexpect
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 7 installs, 0 updates, 0 removals
- Installing facebook/difflib (v1.1): Loading from cache
- Installing hhvm/hsl-experimental (v4.0.1): Loading from cache
- Installing hhvm/type-assert (v3.3.1): Loading from cache
- Installing facebook/hh-clilib (v2.1.0): Loading from cache
- Installing hhvm/hhast (v4.0.4): Loading from cache
- Installing hhvm/hacktest (v1.4): Loading from cache
- Installing facebook/fbexpect (v2.5.1): Loading from cache
Writing lock file
Generating autoload files
/private/var/folders/3l/2yk1tgkn7xdd76bs547d9j90fcbt87/T/tmp.xaQwE1xE/vendor/autoload.hack
$
Adding Functions or Classes
As a toy example, we're going to create a function that squares a vector of
numbers; save the following as src/square_vec.hack
:
use namespace HH\Lib\Vec;
function square_vec(vec<num> $numbers): vec<int> {
return Vec\map($numbers, $number ==> $number * $number);
}
If you then run hh_client
, it will tell you of a mistake:
src/square_vec.hack:4:10,57: Invalid return type (Typing[4110])
src/square_vec.hack:3:53,55: This is an int
src/square_vec.hack:4:40,56: It is incompatible with a num (int/float) because this is the result of an arithmetic operation with a num as the first argument, and no floats.
src/square_vec.hack:3:35,35: Here is why I think the argument is a num: this is a num
To fix this, change the return type of the function from vec<int>
to vec<num>
.
We now have a function that is valid Hack, but it's not tested, and nothing calls it.
Adding an Executable
Save the following as bin/square_some_things.hack
:
#!/usr/bin/env hhvm
require_once(__DIR__.'/../vendor/autoload.hack');
<<__EntryPoint>>
async function main(): Awaitable<noreturn> {
\Facebook\AutoloadMap\initialize();
$squared = square_vec(vec[1, 2, 3, 4, 5]);
foreach ($squared as $square) {
printf("%d\n", $square);
}
exit(0);
}
This program:
- requires and initializes the autoloader so that the function we just defined can be found
- calls the function
- prints the results
- exits
The <<__EntryPoint>>
annotation marks this function as the point where
execution starts - there is nothing special about the function name main
.
You can now execute your new program, either explicitly with HHVM, or by marking it as executable:
$ hhvm bin/square_some_things.hack
1
4
9
16
25
$ chmod +x bin/square_some_things.hack
$ bin/square_some_things.hack
1
4
9
16
25
Linting
Most projects use a linter to enforce some stylistic choices that are not
required by the language, but help make the project consistent; HHAST is the
recommended linter for Hack code. HHAST's linter is enabled by an
hhast-lint.json
file in the project root. A good starting project is to enable
all linters for all directories that contain source code - to do this, save
the following as hhast-lint.json
:
{
"roots": [ "bin/", "src/", "tests/" ],
"builtinLinters": "all"
}
When you ran composer require
earlier, HHAST was installed into the vendor/
subdirectory, and can be executed from there:
$ vendor/bin/hhast-lint
Function "main()" does not match conventions; consider renaming to "main_async"
Linter: Facebook\HHAST\Linters\AsyncFunctionAndMethodLinter
Location: /private/var/folders/3l/2yk1tgkn7xdd76bs547d9j90fcbt87/T/tmp.xaQwE1xE/bin/square_some_things.hack:5:0
Code:
>
><<__EntryPoint>>
>async function main(): Awaitable<noreturn>
Unit Testing
HackTest is used to create unit test classes, and fbexpect is used to
express assertions. Let's create a basic test as tests/MyTest.hack
:
use function Facebook\FBExpect\expect;
use type Facebook\HackTest\{DataProvider, HackTest};
final class MyTest extends HackTest {
public function provideSquaresExamples(): vec<(vec<num>, vec<num>)> {
return vec[
tuple(vec[1, 2, 3], vec[1, 4, 9]),
tuple(vec[1.1, 2.2, 3.3], vec[1.1 * 1.1, 2.2 * 2.2, 3.3 * 3.3]),
];
}
<<DataProvider('provideSquaresExamples')>>
public function testSquares(vec<num> $in, vec<num> $expected_output): void {
expect(square_vec($in))->toBeSame($expected_output);
}
}
We can then use HackTest to run the tests:
$ vendor/bin/hacktest tests/
..
Summary: 2 test(s), 2 passed, 0 failed, 0 skipped, 0 error(s).
If we intentionally add a failure, such as tuple(vec[1, 2, 3], vec[1, 2, 3])
,
HackTest reports this:
$ vendor/bin/hacktest tests/
..F
1) MyTest::testSquares with data set #3 (vec [
1,
2,
3,
], vec [
1,
2,
3,
])
Failed asserting that vec [
1,
4,
9,
] is the same as vec [
1,
2,
3,
]
/private/var/folders/3l/2yk1tgkn7xdd76bs547d9j90fcbt87/T/tmp.xaQwE1xE/tests/MyTest.hack(15): Facebook\FBExpect\ExpectObj->toBeSame()
Summary: 3 test(s), 2 passed, 1 failed, 0 skipped, 0 error(s).
Configuring Git
The vendor/
directory should not be committed; to make dependencies available
on another system or checkout, use composer install
. This will use the
generated composer.lock
file (which should generally be committed) to install
the exact same versions.
$ echo vendor/ > .gitignore
If you're creating a library, users of your library probably don't want your
unit tests - and if they have them, they will need to have fbexpect
and
hacktest
installed in compatible versions to not get Hack errors.
As Composer uses GitHub releases which are automatically generated via
git export
, the simplest solution is to configure git export
to ignore
the tests/
directory:
$ echo 'tests/ export-ignore' > .gitattributes
Configuring TravisCI
We recommend using Docker on TravisCI for continuous integration of Hack
projects. This is usually done by creating a separate .travis.sh
which
executes in the container. For example, a .travis.yml
might look like this:
sudo: required
language: generic
services: docker
env:
- HHVM_VERSION=latest
- HHVM_VERSION=nightly
install:
- docker pull hhvm/hhvm:$HHVM_VERSION
script:
- docker run --rm -w /var/source -v $(pwd):/var/source hhvm/hhvm:$HHVM_VERSION ./.travis.sh
... and a corresponding .travis.sh
:
#!/bin/sh
set -ex
apt update -y
DEBIAN_FRONTEND=noninteractive apt install -y php-cli zip unzip
hhvm --version
php --version
(
cd $(mktemp -d)
curl https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
)
composer install
hh_client
vendor/bin/hacktest tests/
if !(hhvm --version | grep -q -- -dev); then
vendor/bin/hhast-lint
fi
With this configuration, TravisCI runs will check for hack errors, unit test
failures - and on release builds, run hhast-lint
. We do not run hhast-lint
on -dev
builds as hhast-lint
depends on implementation details of HHVM and
Hack which change frequently.