LoginSignup
0
1

More than 5 years have passed since last update.

Hack Standard Library - Experimental Additionsの巻

Posted at

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1