Posted at

Hack Standard Library - Experimental Additionsの巻

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を予定しています。

お楽しみに!!!!





  1. https://hhvm.com/blog/2018/06/18/hhvm-3.27.0.html 



  2. ちなみにrequireなどはHack内で記述する時に strictモードでは利用できません。