type holyshared = Engineer<mixed>

技術的なことなど色々

jbuilderからduneに変えた

dune1.0がリリースされたので、jbuilderからduneに置き換えました。
作業内容としてはそんなに修正箇所はありませんでした。

github.com

作業内容

  1. jbuildファイルの変更
    • jbuildファイルをduneにリネーム
    • 設定ファイルの変更
  2. コマンドをjbuilderからduneに変更
  3. opamファイルの変更
    • 依存しているパッケージをjbuilderからduneに変更
  4. コンパイル時のwarningメッセージを出ないように修正
    • コンパイル時のオプションが変わったのか、warningがでるようになったのでソースを修正

1. jbuildファイルの変更

jbuildファイルをduneに名前を変えた後に、S式の()を一つ外します。
duneではいらなくなったようです。

削除し忘れていても、ビルド時にメッセージで()がいらないと言われるので、すぐ直せます。

不要な()を削除する

修正前
(library (
  (public_name typesafety.misc)
  (name misc)
))
修正後
(library
  (public_name typesafety.misc)
  (name misc)
)

パラメメータの置き換え

パラメータは${}で指定できましたが、duneからは%{}に変わったようです。  

(rule (
  (targets (github_t.ml github_t.mli))
  (deps (github.atd))
  (action (run ${bin:atdgen} -t ${<}))
))
(rule
  (targets github_t.ml github_t.mli)
  (deps github.atd)
  (action (run %{bin:atdgen} -t %{deps}))
)

2. コマンドをjbuilderからduneに変更

これは単純にjbuilderをduneに置き換えるだけです。

修正前

build:
    jbuilder build

修正跡

build:
    dune build

3. opamファイルの変更

これも単純にjbuilderをduneに置き換えるだけです。

修正前

build: [
  ["jbuilder" "build" "-p" name "-j" jobs]
]
build-test: [
  ["jbuilder" "runtest" "-p" name]
]
depends: [
  "jbuilder" {build}
]

修正後

build: [
  ["dune" "build" "-p" name "-j" jobs]
]
build-test: [
  ["dune" "runtest" "-p" name]
]
depends: [
  "dune" {build}
]

4. コンパイル時のwarningメッセージを出ないように修正

使用していない式や型の定義などは、warningがでるようになりました。
これは単純に直していけばいいので、そんなに難しくないかと思います。

js_of_ocamlをjbuilder(dune)で試してみた

OCamlバイトコードをJSに変換できるツール、js_of_ocamlを試して見ました。
とりあえず、jbuilder(dune)を使用して、JSを出力できるようにはなりました。
今回はビルドの方法だけの内容です。

必要なもの

opam経由で必要なパッケージをインストールします。
必要なパッケージは下記の通りです。
インストールされたバージョンは最新の3.1.0です。

opam install -y js_of_ocaml js_of_ocaml-ppx

jbuildファイルを用意する

ruleに出力するファイルの設定を追加します。
js_of_ocamlのオプションに--source-mapを追加して、ソースマップを出力するようにしています。

また、aliasにDEFAULTを追加して、依存しているファイルも用意するようにしています。
jbuildファイルがあるディレクトリをベースにindex.html、main.jsをコピーします。

(jbuild_version 1)

(executables (
  (names (main))
  (preprocess (pps (js_of_ocaml-ppx)))
))

(rule (
  (targets (main.js))
  (action
    (run ${bin:js_of_ocaml} --source-map ${path:main.bc} -o ${@} --pretty)
  )
))

(alias (
  (name DEFAULT)
  (deps (main.js index.html))
))

index.htmlの用意

main.jsのファイルを読み込む適当なHTMLファイルを用意します。

<!doctype html>
<html>
  <head>
    <title>example</title>
    <script src="main.js"></script>
  </head>
  <body>
  </body>
</html>

main.mlの用意

コードは簡単なものにして、デバッグしやすくします。
このコードは、DOMContentLoadedイベントにイベントハンドラを追加して、デバッグログを出力するだけのコードです。

open Js_of_ocaml

module Html = Dom_html

let document = Html.window##.document
let addEventListener = Dom.addEventListener
let domContentLoaded = Dom_events.Typ.domContentLoaded

let debug f = Printf.ksprintf (fun s -> Firebug.console##log (Js.string s)) f

(* DOMContentLoadedのイベントハンドラ *)
let dom_content_loaded evt =
  debug "debug: %s" "domContentLoaded!!";
  Js.bool true

let () =
  let event_handler = Dom.handler dom_content_loaded in
  ignore (addEventListener document domContentLoaded event_handler (Js.bool false))

Makefileを用意する

とりあえず、ビルドと成果物のクリーンアップをできるようにします。
jbuildのaliasにDEFAULTを指定しているので、@DEFAULTをコマンドの引数に追加するだけで、ビルドできます。

all:
    jbuilder build @DEFAULT

clean:
    jbuilder clean

JSのビルド

ビルドはmakeコマンドを実行するだけです。
__build/default の配下に出力されたJSファイル、index.htmlコピーがあるはずです。
index.htmlファイルをブラウザで開いて、コンソールにデバッグメッセージが出力されていればOKです。

make

Hackで書いたProcessパッケージ

Processパッケージをリファクタリングしました。
https://github.com/hhpack/process

主な変更点は下記の2点です。

  • stream_selectからstream_awaitへの置き換え
  • IDisposableを使用したリソース管理

stream_selectからstream_awaitへの置き換え

パイプの状態監視に今まではstream_selectを使用していました。
これをstream_awaitに置き換えました。

stream_awaitは非同期な関数なので、awaitを使用していたのですが、戻り値のステータスが期待した値を返してくれませんでした。

仕方ないので、\HH\Asio\join関数で処理を待つように変更したところ期待通り動作するようになりました。

stream_awaitは内部的にepollでも使ってるのかな?

before

await stream_await($handle, STREAM_AWAIT_READ, 0.2);

after

\HH\Asio\join(stream_await($handle, STREAM_AWAIT_READ, 0.2));

IDisposableを使用したリソース管理

前の実装までは__destructでプロセスのパイプをクローズしていたのですが、IDisposableが使えるようになったので、置き換えました。

これでブロックスコープを抜けた後などに、パイプをすぐにクローズできるようになりました。

この機能はHHVM3.24以降で利用可能です。 https://hhvm.com/blog/2018/01/16/hhvm-3.24.html

おそらく、C#ユーザーあたりから提案があったのだと思います。(async/awaitもそうだったはず)

<?hh //strict

use HHPack\Process;
use HHPack\Process\ProcessOptions;
use HHPack\Process\Output\{ Stdout, Stderr };

async function pipe_example() : Awaitable<void>
{
  $options = new ProcessOptions();
  $options->stdout(new Stdout());
  $options->stderr(new Stderr());

  using ($p = Process\spawn('hh_client restart', [], $options)) {
    await $p->wait();
  }

  using ($p = Process\spawn('hh_client check --json', [], $options)) {
    await $p->wait();
  }
}

ppx_inline_testの仕様が変わったみたい

ppx_inline_testの仕様が変わったぽいです。
今まで、テスト実行時にコンソールにテスト結果が表示される仕様だったと思うのですが、デフォルトで表示されなくなったようです。
なのでテスト実行されているのかがわかりにくくなっています。

テスト結果を表示したい場合は、-verbose オプションを指定すれば表示できます。
変更はjbuildファイルのテスト設定部分に指定するだけです。

(alias
  ((name runtest)
  (deps (test_runner.exe))
  (action (run ${<} inline-test-runner [ライブラリ名] -verbose -diff-cmd "diff -u --label actual --label expected"))))

f:id:holyshared:20180401205347p:plain

2回目以降のテストに関しては、ファイルに変更がない限りは結果は表示されないようです。

f:id:holyshared:20180401205357p:plain

テストを落ちるように変えてみて、結果が失敗するかもみてみましたが、正常にテストが落ちるので問題ないようです。
久しぶりにmake testを実行してみて、テスト結果が表示されないので、ビルドが期待どうりできてないかと思ってびっくりしましたが、問題なくてよかったです。

Hack用のコードジェネレータの新しいバージョンをリリース

Hack用のコードジェネレータの新しいバージョンをリリースしました。
前のバージョンでは、デフォルトのジェネレータしか使用できなかったのを独自のジェネレータを使用できるようにしました。

サンプルのプロジェクト下記に用意してあります。
https://github.com/holyshared/hhpack-codegen-example

利用する為の設定

composer.json

hhpack/codegenを開発用のパッケージに追加します。

"require-dev": {
  "hhpack/codegen": "^0.2.0"
},

hh_autoload.json

ジェネレータの設定が読み込めるように、devRootsにパスを追加します。
パスが間違っていると、設定が読み込めないので注意してください。

{
  "roots": "src",
  "devRoots": "/path/to/" // 設定ファイルがあるパス
}

ジェネレータの設定ファイルを追加する

名前空間に対して、対応するジェネレータをマッピングするのは変わっていないです。
今回のバージョンではジェネレータに対して、名前と説明をメタ情報と持たせることができます。
このメタ情報は、コマンドラインからジェネレータを指定するのに使用します。

下記の例ではジェネレータにそれぞれ、package:class、package:testclassという名前をつけています。

<?hh //strict

namespace HHPack\Example\Generators;

use HHPack\Codegen\Cli\{ DefinedGenerator };
use HHPack\Codegen\Contract\{ GeneratorProvider };
use HHPack\Codegen\HackUnit\{ TestClassGenerator };
use HHPack\Codegen\Project\{ PackageClassGenerator };
use function HHPack\Codegen\Cli\{ namespace_of, define_generator };

final class Generators implements GeneratorProvider {
  public function generators(): Iterator<DefinedGenerator> {
    // Link package namespace to generator
    yield define_generator('package:class', 'generate class file for package')
      ->mapTo(
        namespace_of('HHPack\Example', 'src')->
          map(PackageClassGenerator::class)
      );

    // Link package test namespace to generator
    yield define_generator('package:testclass', 'generate test class file for package')
      ->mapTo(
        namespace_of'HHPack\Example\Test' 'test')->
          map(TestClassGenerator::class)
      );
  }
}

コードの生成

コードを生成するにはコマンドラインからジェネレーターを指定します。
利用できるジェネレータは、ヘルプで確認できます。

vendor/bin/codegen -h

Usage: codegen [OPTIONS] [GEN] [NAME]

Arguments:
   GEN: generator name (ex. lib, test)
    package:class       generate class file for package
    package:testclass   generate test class file for package

  NAME: generate class name (ex. Foo\Bar)

Options:
  -h, --help     Display help message
  -v, --version  Display version

今回の例だと、package:class、package:testclassを利用できます。

vendor/bin/codegen package:class [CLASS_NAME]
vendor/bin/codegen package:testclass [CLASS_NAME]

例えば下記の例だと、src/FooBarのクラスファイルを生成します。

vendor/bin/codegen package:class Foo\\Bar

独自ジェネレータ

独自のジェネレータを使用したい場合は、インタフェースのHHPack\Codegen\Contract\ClassFileGeneratableを実装すればいいです。
詳しくはデフォルトで組み込まれているソースを参考にしてください。

use HHPack\Codegen\{GenerateClassFile};
use HHPack\Codegen\Contract\{ClassFileGeneratable};
use Facebook\HackCodegen\{ICodegenFactory, CodegenFile, CodegenClass};

final class MyGenerator implements ClassFileGeneratable {
  public static function from(ICodegenFactory $factory): this {
    return new self($factory);
  }

  public function generate(GenerateClassFile $target): CodegenFile {
  }
}

HackでのJSONのパース

HackでJSONをパースする際にjson_decodeを使うと思うのですが、JSON_FB_HACK_ARRAYSが新しく指定できるようになったみたいなので調べて見ました。

また、JSONを扱う場合の実装をどうするかも検討しました。

json_decodeのオプションによる型の違い

オプションなし

オプションを指定しない場合、パースした結果はarray型になるようです。

function test1(): array<string, mixed> {
  $j = file_get_contents("test.json");
  $v = json_decode($j, true);
  var_dump($v);
  return $v;
}

出力

array(2) {
  ["name"]=>
  string(3) "Bob"
  ["group"]=>
  array(1) {
    ["name"]=>
    string(9) "HHVM/Hack"
  }
}

JSON_FB_COLLECTIONS

このオプションの場合、パースした結果はMap型になるようです。

// JSON_FB_COLLECTIONS
function test2(): Map<string, mixed> {
  $j = file_get_contents("test.json");
  $v = json_decode($j, true, 512, JSON_FB_COLLECTIONS);
  var_dump($v);
  return $v;
}

出力

object(HH\Map)#1 (2) {
  ["name"]=>
  string(3) "Bob"
  ["group"]=>
  object(HH\Map)#2 (1) {
    ["name"]=>
    string(9) "HHVM/Hack"
  }
}

JSON_FB_HACK_ARRAYS

このオプションの場合、パースした結果はdict型になるようです。
JSONの構造が静的解析時にわからないので、dict<string, mixed>扱いになりそうです。

// JSON_FB_HACK_ARRAYS
function test3(): dict<string, mixed> {
  $j = file_get_contents("test.json");
  $v = json_decode($j, true, 512, JSON_FB_HACK_ARRAYS);
  var_dump($v);
  return $v;
}

出力

dict(2) {
  ["name"]=>
  string(3) "Bob"
  ["group"]=>
  dict(1) {
    ["name"]=>
    string(9) "HHVM/Hack"
  }
}

通常はどうするか

普通にJSONを扱うコードを書く分には、次のようにするのをお勧めします。

  1. 構造の表現にshapeを使用する
  2. type-assertを使用する

1. 構造の表現にshapeを使用する

dict、array、Mapで型指定してしまうと、どうしてもmixedを使用する羽目になるので、タイプチェッカーと相性が悪くなります。
mixedはnullを含む、全てのものが値として考えられるので、極力使うべきではないと思います。 代わりにJSONのフォーマットをshapeで表現します。

/**
 * 次のようなJSONの場合
 *
 * {
 *   "name": "Bob",
 *   "group": {
 *     "name": "HHVM/Hack"
 *   }
 * }
 */

type GroupJSON = shape(
  "name" => string
);

type UserJSON = shape(
  "name" => string,
  "group" => GroupJSON
);

shapeにした場合、定義されていないフィードに対してのアクセスは静的解析時に検出することができます。
パースした結果を参照したりするコードに非常に有効です。

2. type-assertを使用する

type-assertパッケージを使用して、実行時にチェックを行うようにします。 https://github.com/hhvm/type-assert

パースするJSONをファイルから読み込んだりする場合、期待する構造のものかは実行時にしか基本的にはわからないため、静的解析時の問題検出は諦めて、実行時に検出する方針です。

TypeAssert\matches_type_structureTypeStructure<T>と検査対象の値を受け取り、値が型の定義を満たしているか検証します。
値が仕様を満たしている場合は、Tの値をそのまま返します。

TypeStructure<T>を得るにはtype_structure関数を使用します。
第1引数にclass名、またはオブジェクト、第2引数にはType Constantの名前を指定します。

この例では、JSON形式のサーバーの設定を読み込む例です。

function server_config_from_json(string $path): ServerConfiguration {
  $content = file_get_contents($path);
  $rawConfig = json_decode($content, true, 512, JSON_FB_HACK_ARRAYS);
  $validConfig = TypeAssert\matches_type_structure(
    type_structure(ServerConfiguration::class, 'T'),
    $rawConfig
  );
  return new ServerConfiguration($validConfig);
}

ServerConfigurationの定義はこうなっています。

//JSONの設定ファイル
interface JSONConfiguration {
  abstract const type T; //ここの型はインターフェースを実装するクラスで指定する
}

final class ServerConfiguration implements JSONConfiguration {
  //JSONのスキーマを指定する
  const type T = shape(
    "host" => string,
    "port" => int
  );

  public function __construct(private this::T $json) : void
  {
  }

  public function host() : string {
    return $this->json['host'];
  }

  public function port() : int {
    return $this->json['port'];
  }
}

困りごと

TypeStructure<T>は基本的にtype_structure関数でしか、生成できない見たいです。
ReflectionTypeAliasのgetTypeStructureメソッドでいけないかなと思ったのですが、hhiファイルの定義上はarrayを返すようになっていて、 TypeStructure<T>ではないとタイプチェックでエラーになりました。

TypeStructure<T>はshapeなので、shapeを生成するコードを書くしかないぽいです。

Scheme手習い読み終わった

定理証明手習いを途中まで読んでいたのですが、読む前におすすめされていたのと、近所の本屋に一冊だけあったので読みました。

最初にちょっとだけ読み始めて、載っているコードが非常に少なく、写経しなくても覚えれそうと思い、 ちょうど真ん中ぐらいの内容まで、読むだけにしていました。

しかし、だんだん読むのに飽きてきたので、写経するように変えていくようにしました。

本の内容

基本的にSchemeになれるようにするためのドリルぽい印象です。
実用的なコードはないですね。

  • 7割ぐらいリスト操作
  • 再帰するコードがひたすら出てくる
    • OCamlとかやっている人はそんなに難しくない
  • ドリル形式、読み手に質問しつつ、書く関数の仕様をはっきりさせていくスタイル
    • 仕様わかったよね?、もう書けるよね?、じゃあ書いてみようのステップ
  • cond, car, cdr, consをよく使う、リストの再帰だから仕方ない
  • 1つの関数が大したことないので、サクサク進める

写経の仕方

写経するに準備したもの

用意したのはSchemeの処理系だけです。 SchemeLISPはやったことなかったので、インストールが楽そうなRacketにしました。

写経の仕方

Githubリポジトリ作って、写経したその日にやったことをREADME.mdに雑に書いていく感じのスタイルです。

コードは本の内容を移すのではなく、期待する動作をするコードを書いていきました。
なので、本のこういう関数に引数x,yを与えると、結果はどうなるか、という仕様部分しか見てないです。

そして、書いた結果と本のコードを見比べて、条件が足りているか答え合わせをする感じです。

感想

Scheme入門的にはいいかなと思います。

プログラミングやったことない人にはいいかもしれないですが、ただ、内容が割と単調なので、途中で飽きる人出そうな気はします。

ちなみにSchemeやったことなかった人の意見です。

OCamlと比べて、Scheme再帰が書きにくいと感じました。

これは、OCamlでListの再帰を書くとき、自分は普段パターンマッチを使用していて、特定の条件に対して、それぞれコードを書いていくスタイルなのですが、Schemeの場合はリストが空の場合はこう、リストの先頭がこれの場合こうと手順を意識しないといけない印象を受けた為です。

この例は、あんまりよくないです。

OCaml

(* [] ->, hd::tail ->の順番を変えても問題ない *)
let print_all l =
  let rec print_all l =
    match l with           (* matchじゃなくて、functionの方がいいかも *)
      | [] -> ()           (* 空の場合 *)
      | hd::tail ->        (* 空でない場合 *)
        print_endline hd;
        print_all tail in
  print_all l

Scheme

(define print_all
  (lambda (l)
    (cond 
      ((null? l) values)         ; null?から先に確認
      (else (begin
        (display (car l))
        (newline)
        (print_all (cdr l)))))
))

書いた方が覚えやすいです

途中から写経し始めたわけですが、読んでいるだけだと記憶の定着率が悪いのを再認識しました。
書けるだろうと、書き始めて見ても、もどう書けばいいのかなかなか頭から出ませんでした。