type holyshared = Engineer<mixed>

技術的なことなど色々

Golangで五目並べ作った

そういえば、Golangでコード書いたことないや、と思って作ってみました。

Golangはコード読んだことあるけど、書いたことがなかったので、作り始める前に、 Golangチュートリアルを半分くらいやったり、出勤時間にパッケージのAPI調べてたしました。

https://github.com/holyshared/go-gobang

五目並べ

ゲーム仕様

できるだけ簡単な方が作りやすいと思ったので、五目並べの仕様は次の通りにしました。

  • ボードは25 * 25のサイズ
  • プレイヤーは白/黒の石を選べる
  • プレイヤーはNPCのプレイヤーと対戦する
  • プレイヤーが先行
  • 再対戦できる
  • フロント部分は頑張らない(Golangの学習が目的なので)

NPCのAI

NPCのAIを実装したのですが、「負けないようにするにはどうするか」という観点で実装してみました。 AIの思考パターンは次のようにしました。

  1. 自分の石が4つ揃っている場合、5つにする(絶対に勝つ為)
  2. 相手の石が4つ揃っていて、かつ隣接するマスの片方が空いてる場合、邪魔をする(絶対に揃えさせない)
  3. 相手の石が3つ揃っていて、かつ隣接するマスの両方が空いてる場合、邪魔をする(次の自分のターンで絶対に揃えさせない)
  4. 揃いそうなやつを増やす
    1. 自分の石が3つ揃っている場合、揃える為の残り2つのマスの空きからランダムに選ぶ
    2. 自分の石が2つ揃っている場合、揃える為の残り3つのマスの空きからランダムに選ぶ
    3. 自分の石が1つ揃っている場合、揃える為の残り4つのマスの空きからランダムに選ぶ
  5. どれもない場合、空いてるマスを選ぶ

これだけでもそこそこ強くなりました。

感想

設計どうするか、かなり悩みました。 マスが空いてるか、空いてないかの状態どうやって表現しようかとか(RustだったらOptionで表現するのになー)、 パッケージ分けようとして、パッケージ間でimportし合ってビルドがうまくいかなかったり....。

言語仕様がシンプルな分、ちょっと型の表現力が弱いイメージがやっぱりありますね。
そのかわり、学習コストが低いのは良かったです。

先週、みんなのGo言語買ったので、読んだ後に自分のコードもう一回読んでみます。

Hacklangのパッケージのテストコードを、型チェックかかるようにしたかったが断念した話

f:id:holyshared:20160305171858p:plain

タイトルの通りHacklang用のパッケージを開発していて、テストコードも型のチェックしたかったので、 できるか試してみました。

今、自分がパッケージ開発で使用しているテストツールは下記の通りです。

この構成だと、ソースはHackで書いて、テスト自体はPHPのエコシステムを使う感じになります。
これはこれでいいのですが、テストコードをpartialモードにする必要があり、テストコード内の型の矛盾などが、静的解析でチェックできない場合があります。

その場合、テストコードの型矛盾などの問題は、ランタイム時にようやく判明する可能性が高くなります。
極力そうゆうはことにはしたくないので、じゃあテストコードもstrictで書けばよくね?ってことになります。

Hacklangでテストコード書くには

基本的にテストコードをHackで書くには、次の方法があると思います。

  1. 既存のPHPのテスティングフレームワークを使用しつつ、テストコードをHackで書く
  2. Hacklang用のテスティングフレームワークを利用する

1の場合は既存のPHPテスティングフレームワークはそのままで、テストコードだけHackで書く方法です。
この場合、PHPのコードで、型の解決ができない部分がどうしてもでてきます。
じゃあその部分はどうするかというと、型の定義ファイルを作成して補います。

Hackのモードでdeclモードというものがあって、クラスや関数のシグニチャをファイルに書くと、型チェックの際に定義ファイルを参考に型のチェックを行ってくれます。
これでテストコードも型のチェックの恩恵を受けれるようになります。

2の場合は、そのままんまの意味で、HackUnitなどのHack用のテスティングフレームワークを利用します。

この場合、素直にHackでテストコード書けばいいので手間がかかならいです。

型の定義ファイル書くのだるいので、HackUnitで

自分は型の定義ファイル書きたくなかったので、HackUnitでテストコード書くことにしました。
機能が少なくて、学習コストはそんな高くなくさくっと導入できました。
ただ、型のチェックまだごまかしてるところが若干あります。

リリース後に気づいた問題

無事テストコードもHackで書けたのでリリースしまして、利用しているパッケージの方のバージョンあげようと思い、composer update実行し、型のチェックをかけたら、テストコードで型エラーがでるようになりました。

Unbound name: HackPack\HackUnit\Contract\Assert (an object type)

HackUnitのクラス名が解決できてないようで、なんでかなと思ったら、基本的に依存してるパッケージの開発用に使用するパッケージはインストールされないのを忘れていました

下のような場合、依存しているパッケージsome_packageのrequire-devで指定されているパッケージはインストールされないので、型のチェックが通りません。

.hhconfig
src
vendor
    /some_package < このパッケージのテストコードが型チェックに引っかかる。

なので、パッケージ開発している時はいいのですが、作ったパッケージを利用したさらに別のパッケージ作る場合は注意が必要です。 基本的に型をチェックをかけたくないコードのフィルタリングがhh_clientできないので、完全に詰んでる状態です。

flowtypeとかは、設定ファイルでその辺をコントロールできるのですが、何でhh_clientとはできないないのか謎ですね。

結果どうしたか

revertして、元に戻しました。
いまさら、revertして戻したくないなーと思ったのですが、いい解決方法がなかったので….
composerの設定でテストコードを除外して配布見たいなのができればいいのですが、できそうになかったし…

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