Hackの開発をサポートしてくれる hhvm/hsl
今回はそんなライブラリの追加機能的なライブラリの紹介です
将来hslに追加される機能が含まれていたりしますので、
一部を先取りできます
hhvm/hsl-experimental
このライブラリは、当然Hackでしか動作しません。
依存関係は次の通りです。
特殊なものはありませんが、hhvm/hslが必要となります。
"require": {
"hhvm": "^3.28.0",
"hhvm/hhvm-autoload": "^1.4",
"hhvm/hsl": "dev-master"
}
filesystemや、io(streamなど)、
正規表現など、どれもHackならではの実装になっているので、
Hackにある程度慣れた方は実装を見たり、
使ってみたりすると良いでしょう。
今回も一部の機能などを紹介します。
どれも実際の開発で利用できるものなので、知っておいて損はありません!
Regex
Hackの正規表現ですが、独自の記述方法が実はあります。
re"I am not a regular expression"
re以下は正規表現のパターンが記述できるようになっています。
利用する場合は次のようになります。
public static function checkThrowsOnInvalidRegex<T>(
(function (string, Regex\Pattern<shape(...)>): T) $fn,
): void {
/* HH_FIXME[4275] Hack release */
expect(() ==> $fn('foo', re"I am not a regular expression"))
->toThrow(
Regex\Exception::class,
null,
'Invalid regex should throw an exception',
);
}
public function testThrowsOnInvalidRegex(): void {
self::checkThrowsOnInvalidRegex(($a, $b) ==> Regex\first_match($a, $b));
self::checkThrowsOnInvalidRegex(($a, $b) ==> Regex\matches($a, $b));
self::checkThrowsOnInvalidRegex(
($a, $b) ==> Regex\every_match($a, $b));
self::checkThrowsOnInvalidRegex(($a, $b) ==> Regex\replace($a, $b, $a));
self::checkThrowsOnInvalidRegex(($a, $b) ==> Regex\split($a, $b));
}
Hack独自の記法のため、PHPから見るとびっくりするのようなコードに見えるかもしれません。
コンパクトに記述できるので、個人的には気に入っています。
Regexで用意されているメソッドは、
PHPの一般的な正規表現系の関数と大差はありません。
大体のメソッドは内部でregex_matchをコールしています。
function regex_match<T as Regex\Match>(
string $haystack,
Regex\Pattern<T> $pattern,
int $offset = 0,
): ?(T, int) {
$offset = validate_offset($offset, Str\length($haystack));
$match = darray[];
$status = @\preg_match(
$pattern,
$haystack,
&$match,
/* HH_IGNORE_ERROR[2049] Private constant */
/* HH_IGNORE_ERROR[4106] Private constant */
\PREG_FB__PRIVATE__HSL_IMPL | \PREG_OFFSET_CAPTURE,
$offset,
);
if ($status === 1) {
$match_out = darray[];
foreach ($match as $key => $value) {
// TODO(T35726135) remove when HHVM fix is released.
$match_out[$key] = $value is string ? $value : $value[0];
}
$offset_out = $match[0][1];
/* HH_FIXME[4110] Native function won't have this problem */
return tuple($match_out, $offset_out);
} else if ($status === 0) {
return null;
} else {
throw new Regex\Exception($pattern);
}
}
見慣れぬ定数はHack専用の定数で、PHPにはないものです。
Regex\MatchやRegex\Patternは、下記のもので、
hhvm に含まれているものです。
namespace HH\Lib\Regex {
type Match = shape(...);
newtype Pattern<+T as Match> = string;
}
ライブラリを紐解きながらコードを読むとHackの面白さが倍増しますのでオススメです。
Io
Ioはphpの入出力系を扱うものです。 php://
などおなじみのものです。
入出力は以下のように利用します。
<?hh // strict
use namespace HH\Lib\Experimental\IO;
list($r, $w) = IO\pipe_non_disposable();
ちなみにDisposeはHackで利用できますので、C#などに慣れている方もすんなり利用できます。
Disposeについてはまた別のタイミングで紹介します。
Io\pipe_non_disposable は次の実装になっています。
<?hh // strict
namespace HH\Lib\_Private;
use namespace HH\Lib\Experimental\IO;
final class PipeHandle extends NativeHandle {
public static function createPair(): (this, this) {
/* HH_IGNORE_ERROR[2049] intentionally not in HHI */
/* HH_IGNORE_ERROR[4107] intentionally not in HHI */
list($r, $w) = Native\pipe() as (resource, resource);
return tuple(new self($r), new self($w));
}
}
IO\pipe_non_disposableをコールすると、
tupleでread用とwrite用のリソースがtupleで返却されます。
tupleで返却されるものはlistでそのまま利用できるので、
多値の扱いが非常に簡単になります。この辺りもPHPと違うところですね。
namespace HH\Lib\_Private\Native {
/** Creates a pipe, and a pair of linked file descriptors (resources). The
* first file descriptor is connected to the read end of the pipe; the second
* is connected to the write end.
*
* @return `(resource, resource)`
*/
<<__Native>>
function pipe(): varray<resource>;
}
Native\pipeは上記のものです。(これもhhvm本体にあります)
利用する場合は下記の通りです。
list($r, $w) = IO\pipe_non_disposable();
await $w->writeAsync("Hello, world!\nHerp derp\n");
$read = await $r->readLineAsync();
$read = await $r->readLineAsync();
await $w->closeAsync();
$s = await $r->readAsync();
// $sは '' となります。
このIoはAsyncかそうでないかを選んで利用できます。
asyncである必要がない場合は、rawWriteBlocking。rawReadBlockingなどが利用できます。
状況に合わせて使い分けましょう。
用意されている入出力は次のものです。
namespace HH\Lib\_Private;
use namespace HH\Lib\Experimental\IO;
final class StdioHandle extends NativeHandle {
<<__Memoize>>
public static function serverError(): IO\WriteHandle {
// while the documentation says to use the STDERR constant, that is
// conditionally defined
return new self(\fopen('php://stderr', 'w'));
}
<<__Memoize>>
public static function serverOutput(): IO\WriteHandle {
return new self(\fopen('php://stdout', 'w'));
}
<<__Memoize>>
public static function serverInput(): IO\ReadHandle {
return new self(\fopen('php://stdin', 'r'));
}
<<__Memoize>>
public static function requestInput(): IO\ReadHandle {
return new self(\fopen('php://input', 'r'));
}
<<__Memoize>>
public static function requestOutput(): IO\WriteHandle{
return new self(\fopen('php://output', 'w'));
}
<<__Memoize>>
public static function requestError(): IO\WriteHandle {
/* HH_FIXME[2049] deregistered PHP stdlib */
/* HH_FIXME[4107] deregistered PHP stdlib */
if (\php_sapi_name() !== "cli") {
throw new IO\InvalidHandleException(
"requestError is not supported in the current execution mode"
);
}
return self::serverError();
}
}
前回紹介したMemoizeを利用しています。
つまりリクエストや、CLIのアプリケーションで一度呼ばれると、
それ以降は同じものを取得できるようになっています。
Sealedとは
NativeHandleクラスですが、
残念ながらユーザーが自由に拡張できるようになっていません。
それは何故でしょうか。
Hackならではの機能を一つ紹介しましょう。
<<__Sealed(FileHandle::class, PipeHandle::class, StdioHandle::class)>>
abstract class NativeHandle implements IO\ReadHandle, IO\WriteHandle {
// 省略
}
NativeHandleクラスには、<<__Sealed>>
Attributeが記述されています。
これは、このクラスの継承可能な範囲を明記するもので、
PHPにはありませんが、Java、Scala、C++といった言語にある機能で、
ここに記載されているクラスのみ継承できる仕組みです。
継承したクラスそれぞれがfinalとなっているため、
ユーザーが拡張できるようには現在なっていません。
Sealedは現在のHack公式マニュアルには記述されていないものですが、
3.27から実は利用できるようになっており、
公式のブログ にのみ記載されています。1
HHVM/Hackを利用する場合は、このブログも欠かさずチェックしておくのをお勧めします。
Filesystem
filesystemについてもいくつかHackならではのものが用意されています。2
ファイルへの書き込み例を見てみましょう。
use namespace HH\Lib\Experimental\Filesystem;
$filename = sys_get_temp_dir().'/'.bin2hex(random_bytes(16));
await using $f1 = Filesystem\open_write_only(
$filename,
Filesystem\FileWriteMode::MUST_CREATE,
);
await $f1->writeAsync('Hello, world!');
usingは前述したDisposeに対して利用します。
_Private\DisposableFileHandleクラスが、__disposeAsyncを実装しているため、
利用する場合にusingが必要となります。
<<__ReturnDisposable>>
function open_write_only(
string $path,
FileWriteMode $mode = FileWriteMode::OPEN_OR_CREATE,
): DisposableFileWriteHandle {
return new _Private\DisposableFileHandle(
open_write_only_non_disposable($path, $mode) as _Private\FileHandle,
);
}
__disposeAsyncは以下のものです。
public function __disposeAsync(): Awaitable<void>;
このfilesystemにはPath操作系クラスなども含まれているため、
開発時に多用する場面も多いです。
ライブラリのreadmeに全てが記述されているわけではありませんので、
実装コードを読んだり、テストコードを参照するのがHackを理解する最短の近道だと思います。
この他にも様々な機能がhhvm/hsl-experimentalに含まれていますので、チェックしてみてください。
PHP or HHVM(Hack)?
ここまでくるとPHPとはだいぶかけ離れているのがわかると思います。
これまでHHVMのPHP実行速度や、PHPのちょっと書き方が違う拡張言語、
といった側面で取り上げられることが多いHHVM/Hackでしたが
言語のそのもののアプローチが大きく異なっていることがわかると思います。
静的型付言語に近い形でLLライクに使える言語。
最近の事業サービス系の企業では、
マイクロサービス化などでPHPと並行してGoやScalaといった言語を使い分けて開発する場面も
多くなってきました。
そんな状況の中で、型に強いHackが活躍する場面も多いと思います。
これまでの速度面の観点ではなく、そんな場面で選んでみても良いのではないでしょうか。
次回はMemcachedのルーターとしておなじみのMCRouterとHackを予定しています。
お楽しみに!!!!
-
ちなみにrequireなどはHack内で記述する時に strictモードでは利用できません。 ↩