PHP
hacklang
HHVM

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モードでは利用できません。