Computation
Expressions
for Haxe
terurou
2018-12-22
プロフィール(個人/会社)
• terurou
• デンキヤギ(株) 代表取締役 兼 チーフアーキテクト
• 代表作:デンキヤギ就業規則 *1
• 会社見学に来ると、寿司か焼肉をご馳走します。
• ✉terurou@denkiyagi.jp or @terurou まで
1*1 https://github.com/DenkiYagi/EmployeeHandbook
Haxeとは
2
Haxeとは
• クロスプラットフォームなプログラミング言語
• JavaScript, Flash, C++, C#, Lua, PHP, Java, Python, HL, Neko
• HTML5, Android, iOS, Desktop, Adobe AIR, …
• コンパイルが速い
• JSターゲットの場合、TypeScriptの7.0倍、Dart2の14.4倍速い *1
• 静的型、代数的データ型(GADT)、マクロ(AST変換)
• Haxe 4.0で“関数のアロー構文”がついに実装
3*1 https://github.com/damoebius/HaxeBench
Haxeの使われどころ
• ゲーム開発(これがメイン)
• Dead Cells(The Game Award 2018 Best Action Game受賞),
Northgard, Evoland(Haxe作者が創業した会社が制作) など
• 国内でもHaxeゲーム開発の事例あり
• Webシステム開発
• Backlog *1, Nepula *2 (私が把握しているもの、他にもあるはず)
• 弊社開発案件 (3プロジェクト、長期継続中)
• その他にも、ググってるとHaxeを使ってそうなものが見つかる
4
*1 https://www.wantedly.com/companies/nulab/post_articles/127346
*2 https://nepula.net/nepula
Computation Expressions
に至るまでの軌跡
5
Computation Expressions(コンピュテーション式)とは
制御フローを簡潔に記述する専用構文と、
専用構文を実行できる形に変換するマクロ
もしくは
F#におけるMonad, Monad Transformer
6
F# コンピュテーション式の解説
• 入門(何ができるのかレベル)
• mzpさん:F#プログラマのためのMaybeモナド入門 *1
• 詳細情報
• MSDN F# Computation Expressions *2 (英語版で読むべき)
• bleis-tiftさん:詳説コンピュテーション式 *3
7
*1 http://d.hatena.ne.jp/mzp/20110205/monad
*2 https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions
*3 http://bleis-tift.hatenablog.com/entry/computation-expression
コンピュテーション式、完全に理解した
みなさんがコンピュテーション式を
完全に理解できたので、本題へ
8
デンキヤギの最近の仕事
• JavaScriptは生成するもの、人間が書くものではない
• 静的型付きな言語でかつ代数的データ型を使いたい
• Node.js
• Serverless(Azure Functions, AWS Lambda), Koa.js
• 小難しいWebフロントエンド開発
• SPA, PWA, ServiceWorker, WebAudio, WebAssembly, …
• Adobe AIR(Haxe/Flash) → HTML5(Haxe/JS+OpenFL)へ移植
• Vue.js : 副作用を扱いたい(扱わざるを得ない)事が多い
9
モチベーション
非同期処理というか、
Node.jsとコールバック地獄が
しんどい
10
深いネスト、エラーチェックも必要
ファイルの「アクセス権チェック」「読み込み」だけでつらい
11
const fs = require('fs');
const path = 'hoge.txt';
fs.access(path, fs.constants.R_OK, err => {
if (err == null) {
fs.readFile(path, {encoding:'utf-8'}, (err, data) => {
if (err == null) {
console.log(data);
} else {
console.error(err);
}
});
} else {
console.error(err);
}
}); Node.js
コールバックの分だけ
ネストが深くなる
Node.jsには util.promisify() と async/await がある
• util.promisify()
• Nodebackスタイル関数をPromiseを返す関数に変換できる
• Nodebackとは「第一引数でエラーを受けるコールバック」
• aync/await
• Promiseを使ったコードを手続き型的にフラットに書ける
12
util.promisify() + async/awaitを使ったJSコードに書き換え
ネストがなくなり、手続き型っぽいコードに
13
const fs = require('fs');
const util = require('util');
const access = util.promisify(fs.access);
const readFile = util.promisify(fs.readFile);
const path = 'hoge.txt';
(async () => {
await access(path, fs.constants.R_OK);
return await readFile(path, {encoding:'utf-8'});
})().then(
data => console.log(data),
err => console.error(err)
); Node.js
関数を変換
async/await
でも、Haxeからutil.promisify() + async/awaitを使うのはつらい
• async/awaitはHaxeにはない構文なので、どうする?
• util.promisify()にHaxeでうまく型定義がつけられず、
型情報がすべて消し飛んでしまう大きな問題
14
extern class Util {
function promisify(fn: Function)
: Rest<Dynamic> -> Promise<Dynamic>;
}
引数は単に関数でさえあればいい
コールバック関数という型制約はできない
promisify()で変換後の関数
・ 引数:型・長さの制約なし
・ 戻り値:型不明の値を返すPromise
Haxe
[2018-02-20] Haxe用のpromisify()っぽいものを作成
マクロでコールバック関数をAST解析+型推論して変換
15https://twitter.com/terurou/status/965882934870786048
[2018-03-06] HaxeからJS Nativeのasync/awaitを使えるようにした
マクロ+インラインJSでasync/awaitに変換
16https://twitter.com/terurou/status/970871928255729664
こういう感じで書けるようになった
17
import js.node.Fs;
using hxgnd.PromiseTools;
class Main {
static function main() {
(function () {
var path = "hoge.txt";
Fs.access.callAsPromise(path, Fs.constants.R_OK)
.await();
var data = Fs.readFile
.callAsPromise(path, {encoding:"utf-8"})
.await();
}).asyncCall().then(
function (data) trace(data),
function (err) trace(err)
);
}
} Haxe
しばらく使ってみたが、微妙…
• async/awaitの実装が無理やりなので、実装ミスしがち
• asyncを付け忘れても、Haxe上はコンパイルが通ってしまう
• メソッドにはasyncがつけられない
• Haxeの構文には手が出ないので、対応が難しい
• メソッドの直下にasync functionを書けば済むので許容範囲内
• fn.callAsPromise(…).await() というコードが横に長い
18
[2018-09-17] マクロでHaxe Nativeのasync/await専用構文を作った
そもそも、JS Nativeのasync/awaitも構文糖衣という事に
気が付いて、専用構文をマクロでAST変換
19https://twitter.com/terurou/status/1041685595884380161
JS Nativeのasync/awaitにだいぶ近くなった
20
import js.node.Fs;
import hxgnd.Promise;
using hxgnd.FunctionTools;
class Main {
static function main() {
Promise.compute({
var path = "hoge.txt";
@await Fs.access.callAsPromise(path, Fs.constants.R_OK);
var data = @await Fs.readFile
.callAsPromise(path, {encoding:"utf-8"});
data;
}).then(
function (data) trace(data),
function (err) trace(err)
);
}
} Haxe
Promise.compute()の中で
@awaitが使えるようになった
人間の欲望は果てしない
• この時点で限定的なコンピュテーション式を実装して、
その上にasync/awaitを表現していた
• 蛇足だが、JS以外のターゲットでも動作するようになった
• フルセットのコンピュテーション式も実装可能では?
• 実装すると、専用構文内で if, for, while が使えるようになる
21
[2018-11-23] yieldを除くコンピュテーション式を実装
専用構文をF#のコンピュテーション式風に統一した
22
https://twitter.com/terurou/status/1065790767040360448
https://twitter.com/terurou/status/1065792144516964352
[2018-12-04] コールバック関数専用のコンピュテーション式
最小限のコードで、util.promisify()+async/await相当を
型安全に書けるようにした
23https://twitter.com/terurou/status/1069966913848135681
Node.jsより簡潔に記述できて、型安全!!入力補完も動く!
24
import js.node.Fs;
import hxgnd.NodebackFlow;
class Main {
static function main() {
NodebackFlow.compute({
var path = "hoge.txt";
@do Fs.access(path, Fs.constants.R_OK);
@var data = Fs.readFile(path, {encoding:"utf-8"});
return data;
}).then(
function (data) trace(data),
function (err) trace(err)
);
}
} Haxe
まとめ
25
今後の課題
実装はGitHubで公開 *1 したけど、
ドキュメントを書くのがしんどい
26*1 https://github.com/DenkiYagi/hxgnd
まとめと所感
• Node.jsでのコールバック地獄のこと考えているだけで
1年が溶けたが、型安全かつ簡潔に書けるようになった
• Node.jsとかTypeScriptでは到達できない領域なので満足
• コンピュテーション式を考えた人は、すごく頭がいい
• 発表時間が足りないので内部実装の解説は割愛した
• もしニーズがあれば話すんで呼んでください
• ドキュメントはいつか書きます・・・
27

Computation Expressions for Haxe