type holyshared = Engineer<mixed>

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

js_of_ocamlでモジュールの実装試した

js_of_ocamlで適当なモジュールを実装してみたので、その方法を書いておきます。
モジュールの実装はFFIなどのバインディング実装のような感じで、JavaScriptの型とOCamlの型を意識して実装する必要がありました。

モジュールで提供する関数の引数、戻り値などはJavaScriptの型で書く必要がありました。
OCamlの型のまま実装すると、JavaScriptのデータが期待したものに変換されません。
コード自体はコンパイルできてしまうので、mliファイルを書かずに実装すると問題に気づきにくいです。

今回は次の順番で解説します。

  1. JavaScript型の表現 - これがわからないとモジュールのインターフェース表現できない...
  2. モジュールの定義 - 型の表現がわかると、mliファイルがかける
  3. モジュールの実装 - mliファイルがあるので、仕様どうり実装する

JavaScriptの型表現

JavaScriptからの引数、戻り値は基本的にOCamlの既存の型を使用できません。
なので、js_of_ocamlで用意されている型表現を使用します。  

例えば、OCamlで文字列を受け取って、文字列を返す関数は下記のような記述になります。

val name: string -> string

しかし、js_of_ocamlバイトコードJavaScriptに変換する場合は下記のようにする必要があります。

val name: Js.js_string Js.t -> Js.js_string Js.t

JavaScriptの型を表現するには 'a Js.t を使用します。
型パラメータ 'a には具体的な型を指定します。

代表的な型は下記の通りです。

表記
Number Js.number Js.t
String Js.js_string Js.t
Array ('a Js.js_array) Js.t

例えば、JavaScriptのNumber型の配列表現は ((Js.number Js.t) Js.js_array) Js.t になります。
()を省略して Js.number Js.t Js.js_array Js.t と書いても同じ意味になります。

モジュールの定義

型表現がある程度わかったら、モジュールのインターフェースを記述することができます。
外部に公開するモジュールで下記のようなモジュールのインターフェースがあるとします。
これは num_op というオブジェクトがあり、 increment というメソッドを実装しているという意味になります。

increment は Number型の配列を受け取り、1を足して返すメソッドです。

(* num_op.mli *)
open Js_of_ocaml

val num_op: <
  increment : ((Js.number Js.t) Js.js_array) Js.t
    -> (((Js.number Js.t) Js.js_array) Js.t) Js.meth;
  > Js.t

モジュールの実装

increment は Number型の配列を受け取り、1を足して返すメソッドなので、 Js.array_map を使えば良さそうです。

Js.array_map の仕様は ('a -> 'b) -> 'a Js.js_array Js.t -> 'b Js.js_array Js.t です。
つまり、配列の要素に適用する関数を第一引数に受け取り、第二引数に対象の配列を指定します。

配列の要素はJavaScriptのNumber型なので、このままだと計算できません。
なので Js.float_of_number を使用して、OCamlのfloat型 に変換してから計算します。

また、計算結果は OCamlのfloat型 ではなく、JavaScriptのNumber型 で返す必要があります。
ここを間違えると、バイトコードから生成したJavaScriptが期待した動作をしません。

実装は下記の通りになります。

(* num_op.ml *)
open Js_of_ocaml

let num_op = (object%js
  method increment nums =
    let add_one n = (Js.float_of_number n) +. 1.0
      |> Js.number_of_float in
    Js.array_map add_one nums
end)

let _ =
  Js.export "num_op" num_op

モジュールのコンパイル

js_of_ocamlバイトコードからJavaScriptに変換するので、 ocamlc を指定してコンパイルします。

ocamlfind ocamlc -package js_of_ocaml -package js_of_ocaml-ppx -linkpkg \
  -o num_op.byte num_op.ml

次にバイトコードから、JavsScriptへ変換します。
--no-runtime を指定しない場合、ランタイムのコードも含まれます。

js_of_ocaml num_op.byte

JavaScriptの動作確認

適当なHTMLを用意して、コンソールにログを出力してみます。
ログに結果が表示されていればOKです。

<!doctype>
<html>
  <head>
    <title>example</title>
    <script src="num_op.js"></script>
    <script>
      console.log(num_op.increment([1, 2]));
    </script>
  </head>
  <body>
  </body>
</html>

まとめ

  • JavaScriptのライブラリをOCamlで書くのは辛い、全てOCamlで完結するならいい。
  • コンパイルが通って、JavaScriptが出力されても安心できないので、出力されたJavaScriptのテストコードがいる。
    • そこまでやるならFlow、TypeScriptの方がまだアドバンテージありそうな気がする。
  • コンパイル早いのは嬉しい。
  • JavaScript目当てなら、ReasonMLの方がいいかもしれない。

成果物はgistに置いてあります。 https://gist.github.com/holyshared/3233eb6e5474a0e226ce4216dff0fcd1

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)))))
))

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

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