type holyshared = Engineer<mixed>

技術的なことなど色々

PHPのコードをHackのコードに変換する

PHPコードをHackのコードに置き換えるのは、最高に面倒くさいですよね。
なので、HHVMで提供されている、hackificatorを使用して、一気に変換できないかを検証してみました。

hackificatorPHPコードをHackに置き換える為のコマンドラインツールです。
hackificatorOCamlで実装されており、ASTベースでのコード変換を行いまます。(コード読む限り)

試した、HHVMのバージョンは3.12.0です。

置き換えのステップ

置き換えには2ステップ必要です。

  1. PHPのコードをHackのコードに置き換える
  2. Hackのコードをstrictモードにアップグレードする

PHPのコードをHackのコードに置き換える

下記のコマンドで、指定したディレクトリ内のPHPコードをHackのコードに置き換えます。

hackificator -thrift .

変換作業の仕様は試した感じでは下記のようでした。

  • ファイルのヘッダーを<?phpから<?hhに変える。
    • 変換した直後は、partialモードになる(ヘッダーにpartialコメントはつかない)
  • 変換時に型の矛盾などのエラーを検出した場合は変換しない。
  • 拡張子はPHPのまま。
  • タイプヒントの型の変換は行わない。
    • PHP7のreturn typeのselfをthisに置き換えたりしない。
  • declare(strict_types=1)があるファイルは変換できない。

Hackのコードをstrictモードにアップグレードする

下記のコマンドで、指定したディレクトリ内のHackコードをstrictモードにアップグレードします。

hackificator -upgrade .
  • アップグレードできるものは<?hh //strictにヘッダーを変える。
  • 型エラーが起きるものは変換しない(partialモード or declモードのまま)。
  • 拡張子はそのまま、.hhの場合は、.hhのまま、.phpの場合は.phpのまま。

まとめ

  • PHP7以降のコードベースでも、すんなりとはHackに移行できなさそう。
  • コードの変換はできるが、Hackらしいコードには変換できない。(当たり前だ)
  • 通常のPHPからHackへの移行は次のようになると思います。
    1. PHPからHackへコードを変える。
    2. 型の指定を変える、型の指定を追加する。
    3. 型のチェックを通す。
    4. strictモードへアップグレードする。(本来のHacklang)

Hacklang用にベンチマーク取れるライブラリを作った

f:id:holyshared:20160215130203p:plain

HacklangでのJITの検証とか、Vector、Setのパフォーマンスの計測がしたかったので作りました。
計測用のコードは下記のような感じになります。

namespace hhpack\performance\example;

require_once __DIR__ . '/../vendor/autoload.php';

use hhpack\performance as bench;

function main() : void
{

    bench\sync()->times(15)->run(() ==> {
        $stack = Vector {};

        for ($i = 0; $i < 200000; $i++) {
            $stack->add($i);
        }
    });

}
main();

hhpack\performance\sync関数はベンチマークオブジェクトを返します。
後は、runメソッドで計測したいコードをlamdaで指定します。

非同期処理は、hhpack\performance\syncの代わりにhhpack\performance\asyncを使用します。

namespace hhpack\performance\example;

require_once __DIR__ . '/../vendor/autoload.php';

use hhpack\performance as bench;

async function main() : Awaitable<void>
{
    await bench\async()->times(10)->run(async () ==> {
        // 非同期処理を書く
    });
}
\HH\Asio\join(main());

結果のレポートは標準出力に出ますが、Markdownでも出せるようにするつもりです。
作ってみて、JITが有効になってる時と、無効になってる時での処理速度の違いなど、いろいろ計測できて良いです。

github.com

typechecker-clientで型のカバレッジ取れるようにした

f:id:holyshared:20160114113035p:plain

型チェックの比率が知りたかったので、クライントのライブラリにAPIを新しく追加しました。 ファイル/ディレクトリ単位でデータを抜いて、整形することで見やすいフォーマットに整形して表示したり、 データを別の形式に変換したりすることができます。

簡単な使用方法

基本的に非同期な処理なので、awaitを使用して計算が終わるまで待つ必要があります。
具体的なコードは次のような感じになります。

async function coverage_select_main(string $cwd) : Awaitable<void>
{
    $client = new TypeCheckerClient($cwd);
    await $client->restart();

    $result = await $client->coverage();
    $files = $result->filter(($file) ==> $file instanceof File)
        ->filter(($file) ==> preg_match('/typechecker-client\/src/', $file->name()) === 1);

    echo 'Files:', PHP_EOL;
    foreach ($files as $file) {
        $formattedParsentage = sprintf('%6.2f%%', (float) $file->parsentage() * 100);
        echo $formattedParsentage, ' ', $file->name(), PHP_EOL;
    }
}
coverage_select_main(realpath(__DIR__ . '/../'));

どうやっているのか

HHVMにはhh_clientというコマンドラインプログラムがあります。
こいつを使用すると、型のチェックとカバレッジ計算ができます。

hh_clientには–coverageオプションがあり、実行時に指定するだけで型チェックの比率を確認できます。

hh_client check --coverage [対象ディレクトリ]

また、追加でjsonオプションを指定することで、チェック結果をjson形式で出力できます。

hh_client check --json --coverage [対象ディレクトリ]

typechecker-clientはこの出力されたjsonデータを使用しています。

リポジトリ

hhpackっていうorganizationで公開しているので、PRお待ちしております。

github.com

注意事項

HHVM 3.11.0で試す場合、hh_clientのバグで、プロセスが死なずに残る問題が発生しています。
これはもうすぐパッチがでるはずなので、しばらくまってください。

github.com

まあ、今月末くらいにLTSの3.12.0でるので、3.11.0のことなんてどうでもよくなると思います。

typesafetyっていうパッケージを作った

Screenshot

typesafetyというパッケージを作りました。
Hacklangでコードを書いていると頻繁に型のチェックをかけるのですが、チェックをするのにサーバーを再起動したりしないといけないのでとても面倒でした。
なので、サーバーの起動と型のチェックをまとめてできるようにしました。
特徴として、全てstrictモードで実装されているのと、UNSAFEコメントを使用したアンチパターンを使用していません。

使い方

composerを使用して、インストールします。

composer require hhpack/typesafety

後はコマンドを実行するだけで、型のチェックを行えます。

vendor/bin/typesafety [ROOT_DIRECTORY]

さらに、composer.jsonscriptsに実行できるようにすると楽になります。

{
    "scripts": {
        "check": "vendor/bin/typesafety"
    }
}

これで下記のように簡単に型チェックを行えるようになります。

composer check

CIでの型チェック

CIでテスト実行前に、型チェックをかけることにより、型安全な状態でテストを実行することができます。
型の補償がされていない状態でテストを実行しても、正常に終了する確率が低いので、事前に型のチェックを行うことでテスト時間を短縮できます。

travis-ciの場合は、yamlの設定ファイルに型のチェックを追加することで、実現できます。

script:
  - composer check
  - composer test

typesafetyをリリースするのに別途作ったパッケージ

  • hhpack/process - プロセスを扱うパッケージ、サブプロセスでコマンドを実行できたりする
  • hhpack/typechecker-client - 型チェッカーのクライアントライブラリ、サーバーの起動、停止などで使用する
  • hhpack/color - コンソールの出力をカラーリングするのに使用
  • hhpack/publisher - シンプルなPub/Subライブラリ、型のチェック結果を通知するのに使用

Hacklang用のテスティングフレームワークを作った件

Screen Shot

hhspecifyっていうBDD styleのテスティングフレームワークを公開しました。 下記のコマンドでcomposerを利用してインストールできます。

composer require hhspecify/hhspecify

使い方は下記の通り。

設定ファイルの作成

まず、最初に設定ファイルを作成します。
ファイル名はhhspecify.hhです。

configureメソッドにConfigBuilderを受け付ける、ラムダ式を指定します。 今の所設定できるのが、パッケージとレポーターだけです。

<?hh //partial

use hhspecify\HHSpecify;
use hhspecify\config\ConfigBuilder;
use hhspecify\reporter\SpecificationReporter;

HHSpecify::configure((ConfigBuilder $builder) ==> {

    $package = shape(
        'namespace' => 'vendorname\\spec\\', //スペックファイルの名前空間
        'packageDirectory' => __DIR__ . '/spec' //スペックファイルのディレクトリ
    );

    $builder->package($package)
        ->featureReporter(new SpecificationReporter());

});

スペックファイルの作成

スペックファイルを置くディレクトリにスペックファイルを置きます。
ファイル名はなんでもOKです。

ここではStackSpecというファイル名にします。
implementsのインターフェース指定にSpecificationを指定します。

use hhspecify\Specification;
use hhspecify\feature\FeatureVerifierBuilder as Feature;

final class StackSpec implements Specification
{

    public function __construct(
        private Vector<int> $stack = Vector {}
    )
    {
    }

}

テストメソッドの記述

テストメソッドにFeature属性を指定します。
引数のFeatureVerifierBuilderインスタンスを実行時に受け取るので、 setup/when/then/cleanupの順に検証コードを記述していきます。

  • setup - 初期処理のコードを記述する
  • when - テストをしたい処理をするコードを記述する
  • then - whenを実行した後の結果を検証するコードを記述する
  • cleanup - 後処理のコードを記述する
<<Feature("Vector::add")>>
public function add_value_to_vector(Feature $feature) : void
{
    //setup block - Setup
    $feature->setup(() ==> {
        $this->stack = Vector {}; //Vectorを初期化する
    });

    //when block - Stimulus
    $feature->when(() ==> {
        $this->stack->add(1); //値を追加する
    });

    //then block - Response
    $feature->then(() ==> {
        invariant($this->stack->count() === 1, 'must have been added value'); //値が追加されているか検証する
    });
}

スペックファイルの実行

後は下記のコマンドで、実行するだけです。

vendor/bin/hhspecify

Expectationを再設計した話。

expectationを開発していて、ちょっと気に入らない箇所がいくつか出てきたので、再設計してみた。

再設計したので、expectationはもうメンテしておらず、 expectに変わったので、注意してください。 また、peridotプラグインも、peridot-expect-pluginに変わり、再設計した方のものに変わっています。

再設計後のマッチャーAPIは互換性担保しているはず。
設計を見直した点は、下記の通り。

  1. アノテーションを使用するメリットがそんなになかった。

    旧ロジックはアノテーションでMatcherのどのメソッドを使用して、評価するかを指定する設計だった。 こんな感じで、アノテーションで指定する。

     ```php
     /**
      * @Lookup(name="toEqual")
      * @Lookup(name="toBe")
      * @param mixed $actual
      */
     public function match($actual)
     {
         $this->setActualValue($actual);
         return $this->getExpectValue() === $this->getActualValue();
     }
    
     /**
      * @Lookup(name="toBeTrue")
      */
     public function matchTrue($actual)
     {
         return $this->setExpectValue(true)->match($actual);
     }
     ```
    

    アノテーションエイリアス割り当てられるくらいのメリットぐらいしかなかったので、使わないようにした。

  2. Matcherのソースコードに対する、対応付けがわかりににくい。

    Matcherをソースコードと1対1になるようにした。

    • 例: toBeTrue -> ToBeTrue.php
    • 例: toBeEqual -> ToBeEqual.php
  3. エラーメッセージがわかりにくい。

    rspec-expectationsgomegaあたりが、どうしているか参考にした。

Peridotのプラグインを使用していた人へ

移行はそんな難しくなくて、peridot.phpを編集して、ExpectationPluginの部分をExpectPluginにします。 基本的にはこれでOKなはず。

use expect\peridot\ExpectPlugin;

return function(EventEmitterInterface $emitter) {
    ExpectPlugin::create()->registerTo($emitter);
};

あとはspec書くだけ。

describe('Example', function() {
    describe('#create', function() {
        it('return new Example instance', function() {
            expect(Example::create())->toBeAnInstanceOf('Example');
        });
    });
});

まとめ

いつから俺は、be_truthy/be_falseyの仕様を勘違いしていたのだ

PHPexpectationライブラリ作っているのですが、思いっきり間違った実装をしていることに、rspecのリファレンスを見ていて、気がつきました。

matcherのtoBeTruthy/toBeFalseyが、rspecのmatcherでは、

  • be_truthy - nil、若しくはfalseでない
  • be_falsey - nil、若しくはfalse

なんですね。 自分はずっと、be_true / be_falseのエイリアスだと思っていた...。

リファレンスをみるとちゃんと書いてある。

    expect(actual).to be_truthy    # passes if actual is truthy (not nil or false)
    expect(actual).to be_falsey    # passes if actual is falsy (nil or false)

というわけで、バージョン1.4.3で修正しました。
古いバージョンでは、toBeTruthy/toBeFalseyが、toBeTrue/toBeFalseと等価の振る舞いになっています。