type holyshared = Engineer<mixed>

PHP、Hack、Ruby、Javascript周りの技術ブログ

Hackの型のチェックのエラー内容をレビューコメントとして投稿できるようにした

Hackの型チェックをCI上で行っていたのですが、エラー内容を見るのにCI環境側までいちいち見に行かないといけないので、OCamlでレビューコメントとして投稿できるtypesafety-cliを作りました。

レビューコメントは下記のような感じになります。
https://github.com/hhpack/hackunit-docker-example/pull/3

このプログラムをHHVM + Composerと一緒にパッケージしたDockerイメージを作って、TravisCIでテストの自動化を行っています。(メンテナンスしているパッケージはだいたい置き換え済み)

Docker化したことにより、HHVMのリリースにもすぐに追従できるようになりました。

作った理由

もともと下記のものはHackで作っていました。

  • typechecker-client - hh_clientを扱えるクライアントパッケージ
  • typesafety - エラーメッセージをわかりやすい形に整形して出力するCLIパッケージ

これを機能拡張すればいいのですが、Hackで実装している為、HHVMのバージョンが上がるたびに「これ今動くんかな?」と気にする必要があるので、しばらくメンテナンス放置してると動かなくなる可能性があります。

メンテナとしては、ある程度放置していても動いてくれる方が楽なので、今回の場合はHackをやめて別実装にしました。

OCamlにした理由

最近、趣味でOCamlでコードを書いているのですが、書いていて気持ちが楽なのでOCamlにしています。
基本的にツール周りのコードは年々「雑に書きたい、しかし雑に書いてもそこそこ動いて欲しい」、と思うようになっており、OCamlはその辺があうと思っています。

OOPな言語で書くことが多くて、設計はすぐに思いつくのですが、Rubyですらも書くのはだるいと思ってしまうことが最近多いので、特にクラスの設計とか気にせず、コードを書き始められるのがOCamlのいいところですね。

言語ファイルをチェックできるgemを作った

Screen Shot

言語ファイルをチェックできるgem、i18n_checkerを作りました。

多言語対応をしていると、typoで翻訳されていなかったり、言語ファイルに追加漏れがあったりするので、簡単なチェックができるツールが欲しかったのが作った理由です。

サポートするファイル形式

このgemは下記の形式のファイルをソースとして、翻訳テキストが存在しているかチェックします。
テキストの抜き出しは、ASTを使用してノードから抽出しています。

使い方

使い方はRakefileにタスクを追加するだけです。
タスクのオプションは下記のオプションが使用できます。

オプション名 説明
source_paths ソースファイルのリスト
locale_file_paths 翻訳ファイルのリスト
reporter 検出結果を出力するレポーター
require 'i18n_checker/rake_task'

I18nChecker::RakeTask.new do |task|
  # haml templates, ruby sources
  task.source_paths = FileList['app/models/*', 'app/views/*']

  # locale file paths
  task.locale_file_paths = FileList['config/locales/*']
end

後は、locale_checkタスクを実行するだけで、翻訳テキストが見つからない参照を検出できます。

bundle exec rake locale_check

今後について

今回は翻訳テキストが参照しかチェックしてませんが、使用してない翻訳テキストも調べられるようにするつもりです。

今仕事でやっている案件が実証実験を結構やっていて、実験が終わった後のコードとか、設定が残っていて不要なものをどんどん削除していきたいのでツールで解決できるものは解決していきたです。

OUnitでOCamlライブラリのテストを書く

これはML Advent Calendar 2016の18日目の記事です。

OCamlのテストフレームワークOUnitについて解説します。
また、Oasisの簡単な説明も行います。

プロジェクトのセットアップ

OCamlのプロジェクトでは、だいたいの場合はOasisを使います。
Oasisはビルドの為の支援ツールで、ビルドに必要なMakefileやメタ情報を含んだファイルを設定から自動生成できます。

OasisOpamを使用して簡単にインストールできます。

opam install oasis

インストールが完了したら、設定ファイル_oasisを作成し、必要最低限の情報を記述します。

OASISFormat: 0.4
Name:        example                                        # プロジェクト名
Version:     0.1.0                                          # バージョン
Synopsis:    example                                        # ライブラリの説明
Authors:     Noritaka Horio <holy.shared.design@gmail.com> # 著作者
License:     MIT                                            # ライセンス
Plugins:     META (0.4), StdFiles (0.4), DevFiles (0.4)     # 使用するプラグイン
BuildTools: ocamlbuild                                      # ビルドツール

# ライブラリ名
Library example
  # ソースの場所
  Path:       src
  # ビルドするモジュール
  Modules: Example

もしくは、quickstartコマンドを使用します。
聞かれる質問に答えていくだけで、_oasisファイルを生成できます。

oasis quickstart

_oasisファイルが作成できたら、setupコマンドを実行します。

oasis setup

setupコマンドを実行すると、MakefileやAUTHORS.txt、README.mdなどのファイルが生成されているはずです。

Oasisプラグインで機能を追加できますが、試す分には下記の3つぐらいで十分です。

プラグイン 説明
META ライブラリ用のMETAファイルを生成する
StdFiles _oasisの定義から、README.txt, INSTALL.txt, AUTHORS.txtを生成する
DevFiles ビルドに必要な、Makefileを生成する(なくてもビルドはできます)

ライブラリの実装とテストコード

_oasisの設定で、ビルドするライブラリの指定をします。
ライブラリ名とソースの場所、そしてビルドするモジュールをカンマ区切りで列挙します。

# ライブラリ名
Library example
  # ソースの場所
  Path:       src
  # ビルドするモジュール
  Modules: Example

srcディレクトリを作成して、example.mlファイルを追加して、適当な関数を追加します。
ここでは、文字列を受け取って、文字列をそのまま返す関数を定義します。(解説の為の雑な関数です。)

let say msg = msg

次にこのモジュールのテストコードを書きます。
testsディレクトリを作成して、エントリポイントとなるtest.mlと、モジュール単体のテストコードexample_test.mlを作成します。

テストコードは、OUnitの独自演算子の>::、>:::を使用して記述します。(>:もあるが使うことはないと思う)

example_test.ml

Exampleモジュールのsay関数のテストは、受け取った引数をそのまま返すだけなのでこんな感じになります。

open OUnit2

(* say関数のテスト関数、テストの説明と関数でテストを表現する *)
let say_test =
  "echo string" >:: (fun _ -> assert_equal (Example.say "hello") "hello")

(* このテストモジュールのすべてのテストをまとめる *)
let tests =
  "all_tests" >::: [ say_test; ]

test.ml

エントリポイントではrun_test_tt_main関数でテストを実行するようにします。
ここで各テストジュールをまとめて、指定します。

open OUnit2

let all_tests = "all_tests" >::: [
  Example_test.tests
]

let () =
  run_test_tt_main all_tests;;

演算子の意味

>::

文字列とテスト関数より、新しいテストを返します。

let some_func1 = true

"test desc" >:: (fun ctx -> assert_bool "failed message" (some_func1 ()))

>:::

文字列とテストのリストより、新しいテストを返します。
複数のテストをグルーピングする為に使用します。

let some_func1 = true
let some_func2 = false

"all_tests" >::: [
    "test desc" >:: (fun ctx -> assert_bool "failed message" (some_func1 ()));
    "test desc" >:: (fun ctx -> assert_bool "failed message" (some_func2 ()))
]

テストコードのビルドとテストの実行

テストコードが追加できたら、_oasisファイルにテスト用の設定を追加します。

# テスト用のバイナリの名前
Executable test
 # テストコードの場所
  Path: tests
  # テストコードのエントリポイント
  MainIs: test.ml
  Build$: flag(tests)
  # ビルド結果のフォーマット(バイトコードにもできる)
  CompiledObject: native
  # 配布する必要がないので、false
  Install: false
  # 依存しているモジュールoUnitとテスト対象のライブラリを指定
  BuildDepends: oUnit, example

Test test
  Run$:               flag(tests)
  # テストコマンド
  Command:            $test
  # テストを実行するワーキングディレクトリ
  WorkingDirectory:   tests

テストを実行するには、configureでテスト用に設定を変えた後に、testでテストを実行できます。
これはテストコードを含んだ実行可能なバイナリを生成して、実行しています。

make configure CONFIGUREFLAGS=--enable-tests
make test

まとめ

ここで使用したファイルとかは、ここに適当に書いたOCamlのコードの中にあるので、実物を試したい人はご確認ください。 github.com

Rustでlcovのレポートファイルをマージする

f:id:holyshared:20161207124650p:plain

これは Rust Advent Calendar 2016 (2) の 14日目の記事です。

lcovが出力するレポートのパーサー&マージャーをRustと実装しました。

lcovはgcovの拡張版みたいなもので、gcovの出力ファイルから、HTML形式のようなコードカバレッジレポートを出力するツールです。

lcovは出力の過程で、処理しやすいようにgcovの出力ファイルから独自のレポート形式に変換します。

そのレポートをパース&マージできるようにしたものが、今回実装したライブラリになります。

レポートのパース

lcovのレポートはデータをレコードで表現した単純なテキスト形式のファイルです。
各レコードは次のようになっています。
原文はこちらを参考してください。

No. レコード 説明
1 TN:<test name> テスト名
2 SF:<absolute path to the source file> ソースファイルのパス(絶対パス)
3 DA:<line number>,<execution count>[,<checksum>] 左から行番号、実行された回数、該当行のコードのMD5
4 FN:<line number of function> 関数の開始行
5 FNDA:<execution count>,<function name> 左から関数の実行された回数、関数名
6 FNF:<number of functions found> 見つかった関数の数
7 FNH:<number of function hit> 実行された関数の数
8 LH:<number of lines with an execution> 見つかった行の数
9 LF:<number of instrumented lines> 実行された行の数
10 BRDA:<line number>,<block number>,<branch number>,<taken> 左から行番号、ブロック番号、分岐番号、実行された回数
11 BRF:<number of branches found> 見つかった分岐の数
12 BRH:<number of branches hit> 実行された分岐の数
13 end_of_record 終了を示すレコード、ソースコード単位で出力される

レポートをパースするには、次のようにファイルからパーサーを生成して、1レコードずつパースします。

extern crate lcov_parser;

use lcov_parser:: { LCOVParser, LCOVRecord, FromFile };

fn main() {
    let mut parser = LCOVParser::from_file("../../../fixture/report.lcov").unwrap();

    loop {
        match parser.next().expect("parse the report") {
            None => { break; },
            Some(record) => match record {
                LCOVRecord::SourceFile(file_name) => println!("File: {}", file_name),
                LCOVRecord::EndOfRecord => println!("Finish"),
                _ => { continue; }
            }
        }
    }
}

レポートのマージ

さて本題のレポートマージです。
レポートをマージする際に、気をつけなければならいない点はマージするレポートのバージョンです。

マージしたいレポートが同じバージョンのソフトウェアのレポート場合、マージすることは可能ですが、違う場合は基本的にマージすることができません。

なぜマージできないかというと、どのバージョンの時、コードカバレッジはどの程度だったか?が知りたいわけで、異なるバージョンのレポートをマージすると、正しいレポートにならないためです。

結果
v1+ v1 v1
v1+ v2 マージできない(バージョンが違う)

この為、マージする際にレポートのチェックサムを利用します。
チェックサムは、DAレコードに情報として含まれています。(lcovのchecksumオプションを使用した場合)

このチェックサム値はソースコードの特定の行のMD5値になっており、同じバージョンのソフトウェアの場合、 値が同値になります。

下記の例だと、fixture.cの6行目のMD5値がPF4Rz2r7RTliO9u6bZ7h6gという意味になります。

SF:/Users/holyshared/Documents/projects/lcov-parser/tests/fixtures/merge/fixture.c
......
......
......
......
DA:6,2,PF4Rz2r7RTliO9u6bZ7h6g

このようなチェックを行いつつ、マージしてくれる関数も実装しています。
save_asを使用することで、出力ファイルを指定できます。

extern crate lcov_parser;

use lcov_parser:: { merge_files };

fn main() {
    let trace_files = [
        "../../../tests/fixtures/fixture1.info",
        "../../../tests/fixtures/fixture2.info"
    ];
    let _ = match merge_files(&trace_files) {
        Ok(report) => report.save_as("/tmp/merged_report.info"),
        Err(err) => panic!(err)
    };
}

まとめ

最初は、パーサーだけ実装する予定でしたが、lcovのPerlのコードを読んでいて、ついでだからマージもできるようにするかと思い実装してみました。 既存のプロダクトのコードの移植していく作業は、普段つかっているものの理解力があがるので、勉強になりますね。

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)