Promiseの失敗文脈を使ってはならない理由と組み込み関数では使っていい理由

Promiseのthenメソッドの第二引数およびcatchメソッドのコールバックの呼び出しで表現される失敗文脈、これは原則として使ってはならない。 ネイティブの組み込み関数が使っているからといって真似してPromise値を返し失敗文脈を持つ関数を作ってはならない。

Promiseの失敗文脈には任意の回復可能または予測された失敗と予期せぬ例外を区別できない(例外を強制的に失敗に戻す)欠陥がある。 このためPromiseの失敗文脈の型を定義しようという試みは基本的に無意味だ。 仮に失敗文脈に混入した例外を除去しようとすると以下のようになる。

Promise.resolve()
  .catch(reason => {
    if (reason instanceof Error) throw reason;
    // ...
  });

特定のエラー値を返す場合は以下のようになる。

Promise.resolve()
  .catch(reason => {
    if (reason instanceof Error && reason instanceof DOMError === false) throw reason;
    // ...
  });

とてもバカバカしい。すべての失敗文脈にこの一行が入ったJavaScriptの世界を想像すると頭が痛くなる。 早い話がnullの失敗を繰り返しているのだ。 よってPromiseは原則として失敗を成功文脈で扱い、失敗文脈は例外のみを扱う例外文脈として設計しなければならない。 成否はタプルによる多値で表現し、最初の要素に成否判定のフラグを入れるフォーマットを推奨する。 オブジェクトによる多値は個別に構造を記憶する無駄な労力を要し、生成コストも配列より高いため推奨しない。

new Promise(resolve => resolve([arr.length > 0, arr.pop(), arr]));

一方で組み込み関数が失敗文脈で失敗を表現することは必ずしも間違いではない。 組み込み関数は言語処理系が実装する関数ゆえに絶対に予期せぬ例外を混入させない設計と信頼性確保が可能だからである。 組み込み関数が失敗文脈に例外を含まない(特定のエラー値のみを返す場合を含む)と宣言したらそれは間違いなく含まないとみなせる。 成否表現のフォーマットを強制的に統一させるためといった都合もあるだろう。 同様の意図からライブラリの公開するインターフェイスが失敗文脈の使用を選択する場合もある。 しかしもし失敗と例外が入り混じった失敗文脈を作る関数があればそれはやはり設計が悪い。

対してユーザーの作る失敗文脈付き(Promise値を返す)関数で組み込み関数と同じ信頼性の関数を作ることは容易ではない。 コードサイズが大きくなればなるほどバグにより例外が混入するリスクが増える。 よって失敗文脈を使用する関数を新たに作る場合、その関数は失敗文脈の信頼性を確保するために、低水準で最小単位の関数でなければならない。

単に非同期だからPromiseを使う、Promiseだから失敗を失敗文脈に分離するといった思考論法で作られたPromiseチェインが、要求される失敗文脈の信頼性を自然と備えていることは期待できない。 ゆえに、Promise値を返す組み込み関数を模倣して自身で失敗文脈を持つPromiseチェインや関数を作ることは基本的にアンチパターンである。

async/awaitにおいても同じポリシーが適用でき、async/awaitは原則としてtry-catch文を使用せずに使えるように設計すべきである。 Promiseの失敗文脈を例外文脈として設計した場合、async/awaitのtry-catch文が自然に適合することにも注目してもらいたい。

また、awaitを囲むtryブロックにはawait以外の同期的処理を含めずawaitの失敗文脈の捕捉のみに責務を限定すべきである。 同期処理において捕捉しない例外を非同期処理に限って捕捉しなければならない理由はない。

await sleep(1);
var n = 100;
try {
  await sleep(n);
}
catch (err) {
  throw err;
}

TypeScriptの型検査を無効化して型安全性を失わせる落とし穴5つとその避け方

TypeScriptにはanyを使わないよう注意しても型検査が無効化され型安全性が失われるパターンがいくつかあり、中には致命的なものも存在する。今回はこのパターンをいくつか紹介する。

コールバックのvoid戻り値型

コールバックのvoid戻り値型はすべての型を受け入れるためvoid型 === any型となる。

function f(cb: (a: number) => void): void {
    return cb(0);
}
function g(a: number): number {
    return a; 
}
f(g);

このためvoid型の値はvoid演算子を使用して即座に強制的にvoid型に変換しなければならない。

void f(g);

ここでは手遅れ。

function f(cb: (a: number) => void): void {
    return void cb(0);
}

ここで変換する。

コールバックのパラメータ数

コールバックのパラメータ数の不足は型エラーにならず検出できない。

type F = (cb: (a: number, b: string) => number) => number;
type G = (cb: (a: number) => number) => number;
var f: F = <G>((cb: (a: number) => number) => cb(0));
f((a, b) => a + b.length);

これを回避する方法はない。TypeScript 3.3までに修正済み。

共変・反変

interface A {
  x: string;
}
interface B {
  x: string;
  y: string;
}

const fb = (fa: (a: A) => void) => fa({x: 'x'});
const       fa= (b: B) => alert(b.y);
fb(fa);

まあ、これは仕方ないだろうか。

Generic型

TypeScriptの型検査は基本的に構造型の照合であり、型パラメータは照合の対象となる構造に含まれていないため、構造に反映されない型パラメータの型は無視される。

class C<T extends Foo | Bar> {
    constructor(d) {
        d.cmd();
    }
}
class Foo {
    f;
    cmd() {
        return this;
    }
}
class Bar {
    b;
    cmd() {
        return this;
    }
}
var c: C<Foo> = new C<Bar>(new Bar());

このため型パラメータのみで完結する純粋な幽霊型により型検査を行わせようとしても失敗する。

class C<T> {
}
var c: C<number> = new C<string>();

これを回避するには型パラメータで受け取った型をプロパティやメソッドで持って構造型に反映させなければならない。

class C<T> {
    c: T;
}
var c: C<number> = new C<string>();

このほか、private/protectedアクセス修飾子を使用するとクラスが公称型となり型が一意に識別される(継承クラスの型を除く)。

Generic型定義

TypeScriptがデフォルトで定義しているいくつかの型ではこの性質のため致命的な不整合を見落とす可能性がある。

var s: PromiseLike<number | string> = <PromiseLike<string>>null;
var n: PromiseLike<number> = <PromiseLike<number | string>>s;

どれだけ厳格に型を守っても、上記のように数値のみを返す型が文字列を返すようになる不正な代入が可能である。 型によっては不整合を検出するので不正に代入可能な型を即座に見分けることは難しい。

var s: ArrayLike<number | string> = <ArrayLike<string>>null;
var n: ArrayLike<number> = <ArrayLike<number | string>>s;

この不整合を防ぐにはインターフェイスを拡張して先ほどと同様型パラメータを構造に落とせばよい。

interface PromiseLike<T> {
    _?: T;
}

import/export文を含めると新規の型定義となるため含ないこと。 拡張を行っても外部からは拡張を含まない.d.tsファイルだけを参照させれば拡張を隠蔽できるので外部の型定義を汚染したり競合したりすることはない。

TypeScriptではこの種の不整合によるバグを防ぐため上記のような宣言をおまじないのように毎回追加しておく必要があるだろう。TypeScript 3.3までに修正済み。

まとめ

型があってるのに変な値が入るバグに苦しみたくなかったらここに書いたパターンと対処法を開発メンバーのTypeScript使用者全員に周知することをお勧めする。

TypeScript1.8以降で追加予定または協議中の注目の新機能12つ

TypeScript1.8以降で追加予定または協議中の注目の新機能を紹介します。

この記事はTypeScript アドベントカレンダー2015 25日目の記事です。

戻り値型の厳格な検査(--noImplicitReturns) [Merged]

github.com

github.com

戻り値が定義した型以外になることを禁止します。 必ずオンにしましょう。しない理由がありません。 この調子でFlowtypeと型安全性をガンガン競ってほしいところです。

// The followings are all wrong in a typed context yet the compiler ignores them as a valid code:
function SomeMethod(): number {
    return;
}

function SomeMethod(): number {
    return null;
}

function SomeMethod(): number {
    if (false)
        return 1;
}

function SomeMethod(): number {
    try {
        return 0;
    }
    catch (ex) {
    }
}

Nullable型の禁止(--noImplicitNull) [Suggestion]

github.com

nullやvoidがanyやvoid以外の型に混入しないよう暗黙的なキャストを禁止します。 変数ならconstを使う方法もあるのですがオブジェクトやパラメーターはそうはいきません。 --noImplicitNullオプションでの制御を支持しています。 バグのない世界のために今すぐ+1

let n: number = null; // error
let m: number = undefined; // error

タイプガードへのコントロールフロー解析の適用 [Discussion]

github.com

タイプガードはこれまで条件分岐先のブロックか式にしか適用されませんでしたがタイプガードの結果が以降のすべてのコードに適用されるようになるのでガード節を型解決に使えるようになります。 まだ協議中なのでぜひ+1してきてください。

function foo(x: number | string) {
    if (typeof x === 'string') {
        return;
    }
    // x should now be a number
    if (2 % x !== 0) {
        // do something
    }
}

String Literal Types [Merged]

github.com

変数や関数に与えられる文字列の値を制限できるようになります。 ありがてぇ。

type CardinalDirection = "North"
                       | "East"
                       | "South"
                       | "West";

function move(distance: number, direction: CardinalDirection) {
    // ...
}

let a: "foo" | number = "foo"; // valid
let b: "bar" | "baz" = "foo";  // invalid

Readonly(final)アクセス修飾子 [Scheduled]

github.com

プロパティの再代入を型として禁止し、不変オブジェクトの定義を可能にします。 待ち望まれた機能です。 個人的にはfinalのほうが好きです。

interface Point {
    x: number;
    y: number;
}
interface ImmutablePoint {
    readonly x: number;
    readonly y: number;
}
var pt: ImmutablePoint = { x: 4, y: 5 }; // OK, can convert mutable to non-mutable
pt.x = 5; // Error, 'pt.x' is not a valid target of assignment

var pt2: Point = pt; // Error, cannot convert readonly 'x' to mutable 'x'

// Possibly bad behavior
var pt3: Point = { x: 1, y: 1 };
var pt4: ImmutablePoint = pt3; // OK
pt3.x = 5; // pt4.x is also changed?

// Really bad behavior
/** This function was written in TypeScript 1.0 **/
function magnitudeSquared(v: { x: number; y: number }) {
   return v.x * v.x + v.y * v.y;
}
// Now try to use it with ImmutablePoint
console.log(magnitudeSquared(pt)); // Error, cannot use readonly object in non-readonly call

object型の追加 [Accepted]

github.com

今まで地味にオブジェクト型で制約する型がありませんでしたがこれでようやく制約できるようになります。

出力したJSモジュールファイルの結合 [Merged]

github.com

出力したJSモジュールファイルを1つのファイルに結合します。

Browserifyがいらなくなくなります。 単にオプションを使って結合しただけではrequirejsに依存しますが、以下のスニペットをヘッダとして追加することでブラウザでもNodeでも動作するIsomorphicなライブラリとなります。

gist.github.com

こちらのライブラリで実際にこの機能を使ってBrowserifyなしで同じ相互運用性を実現していますので試してみたい方は参考にするとよいでしょう。

github.com

循環参照が許されませんが、なんくるないさという気持ちになれば問題ありません。

ファイルパスのglobによるパターンマッチ [Scheduled]

github.com

ファイルパスにワイルドカードなどのglobのパターンマッチを使えるようにします。 コンパイルするファイルを何十個も列挙してメンテまでするのはつらすぎます。 v2.xで実装が予定されています。

Union/Intersection typesの型推論強化 [Merged]

github.com

Union/Intersection typeの分解時の型推論が強化され再利用性が向上します。 とてもうれしい。

type Maybe<T> = T | void;

function isDefined<T>(x: Maybe<T>): x is T {
    return x !== undefined && x !== null;
}

function isUndefined<T>(x: Maybe<T>): x is void {
    return x === undefined || x === null;
}

function getOrElse<T>(x: Maybe<T>, defaultValue: T): T {
    return isDefined(x) ? x : defaultValue;
}

function test1(x: Maybe<string>) {
    let x1 = getOrElse(x, "Undefined");         // string
    let x2 = isDefined(x) ? x : "Undefined";    // string
    let x3 = isUndefined(x) ? "Undefined" : x;  // string
}

function test2(x: Maybe<number>) {
    let x1 = getOrElse(x, -1);         // number
    let x2 = isDefined(x) ? x : -1;    // number
    let x3 = isUndefined(x) ? -1 : x;  // number
}

thisの型定義と型推論 [Discussion]

github.com

thisの型定義は避けては通れないのではないでしょうか。 クラスの自己型としてのthisはTS1.7のポリモーフィックthisで実装済みです。

let f = function(this: {data: number}) {
  console.log(this.data2) 
}
let o = {
  data: 12
  f: f
  g: function() {   // this is inferred from the contextual type
    console.log(this.data); 
  }
}
function f(this: {n: number}, m: number) {
    return this.n + m;
}
class C {
  n: number
  m1(this:this, m: number) {
    return this.n + m
  }
  m2(this: {n: number}, m: number) {
    return this.n + m
  }
}

型パラメーターによる制約 [Merged]

github.com

型パラメーターで型パラメーターの制約を作れるようになります。 今まで何度この型を書けずに涙を飲んだかわかりません。

function assign<T extends U, U>(target: T, source: U): T {
    for (let id in source) {
        target[id] = source[id];
    }
    return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };
assign(x, { b: 10, d: 20 });
assign(x, { e: 0 });  // Error

?プロパティーアクセサ [Suggestion]

github.com

CoffeeScriptの便利なやつです。 ESの仕様に入ってほしいくらいです。 ただ最初に提案されたものはx?.y?.zの戻り値の型がx|y|zであまりよろしくありません。

var x = { y: { z: null, q: undefined } };
console.log(x?.y?.z?.foo); // Should print 'null'
console.log(x?.baz); // Still an error
console.log(x.y.q?.bar); // Should print 'undefined'

ミスタイプ(ポスト)はデザイン [Design]

github.com

マイクロソフトによると指がすべって立てられたIssueはデザインによるものだそうです。

草。

まとめ

TypeScriptはバージョンアップを重ねるたびにますます洗練されていき、来年は上述の機能の追加によりさらに躍進が期待されます。

2016年もさらにヒートアップするTypeScriptをよろしく。

JavaScript(TypeScript)でEitherモナドとMaybeモナドを作る

JavaScriptでMaybeモナドのサンプルはよくありますがEitherモナドのサンプルはあまりないのでこれを作ってみます。

この記事はTypeScript アドベントカレンダー2015 24日目の記事です。

まずはMaybeモナドで肩慣らしです。

export class Maybe<T> {
  private MAYBE: [T, void];
  constructor(private thunk_?: () => Maybe<T>) {
  }
  public bind(f: (val: T) => Maybe<T>): Maybe<T>
  public bind<U>(f: (val: T) => Maybe<U>): Maybe<U>
  public bind<U>(f: (val: T) => Maybe<U>): Maybe<U> {
    return new Maybe<U>(() => {
      const m = this.thunk_();
      switch (true) {
        case m instanceof Just: {
          return f((<Just<T>>m).extract());
        }
        case m instanceof Nothing: {
          return <Nothing>m;
        }
        case m instanceof Maybe: {
          return m.bind<U>(f);
        }
        default: {
          throw new TypeError(`Invalid monad value: ${m}`);
        }
      }
    });
  }
  public extract(): T | void
  public extract<U>(defaultValue?: U): T | U
  public extract<U>(defaultValue?: U): T | U | void {
    return this.thunk_().extract(defaultValue);
  }
}

export class Just<T> extends Maybe<T> {
  private MAYBE_JUST: T;
  constructor(private val_: T) {
    super();
  }
  public bind(f: (val: T) => Maybe<T>): Maybe<T>
  public bind<U>(f: (val: T) => Maybe<U>): Maybe<U>
  public bind<U>(f: (val: T) => Maybe<U>): any {
    return new Maybe(() => this).bind(f);
  }
  public extract(): T
  public extract<U>(defaultValue: U): T
  public extract<U>(defaultValue?: U): T {
    return this.val_;
  }
}

export class Nothing extends Maybe<any> {
  private MAYBE_NOTHING: void;
  public bind(f: (val: any) => Maybe<any>): Nothing {
    return this;
  }
  public extract(): void
  public extract<U>(defaultValue: U): U
  public extract<U>(defaultValue?: U): U {
    return defaultValue;
  }
}
import {Maybe as Maybe_, Just as Just_, Nothing as Nothing_} from './maybe-impl';

export type Maybe<T> = Maybe_<T>;
export namespace Maybe {
  export type Just<T> = Just_<T>;
  export function Just<T>(val: T): Just<T> {
    return new Just_(val);
  }
  export type Nothing = Nothing_;
  export const Nothing = new Nothing_();
  export const Return = Just;
}

export type Just<T> = Maybe.Just<T>;
export const Just = Maybe.Just;
export type Nothing = Maybe.Nothing;
export const Nothing = Maybe.Nothing;
export const Return = Just;

基本的な動作を確認をしてみます。

    it('Just', () => {
      const result = Return(0)
        .bind(n => Just(n + 1))
        .bind(n => Just(n + 1).bind(n => Just(`Just ${n}`)))
        .extract('Nothing');
      assert(result === 'Just 2');
    });

    it('Just nest', () => {
      const result = Return(Return(0))
        .bind(m => Just(m))
        .bind(m => m.bind(n => Just(n + 1)).bind(n => Just(`Just ${n}`)))
        .extract('Nothing');
      assert(result === 'Just 1');
    });

    it('Nothing', () => {
      const result = Return(0)
        .bind(n => Just(n + 1))
        .bind(n => Just(`Just ${n}`).bind(_ => Nothing))
        .bind(throwError)
        .extract('Nothing');
      assert(result === 'Nothing');
    });

    it('Nothing nest', () => {
      const result = Return(Return(0))
        .bind(m => m.bind(n => Nothing).bind(throwError))
        .bind(throwError)
        .extract('Nothing');
      assert(result === 'Nothing');
    });

モナド則を満たしているか確認します。

    it('Monad law 1', () => {
      const f = (n: number) => Return(n + 1);
      const x = 0;
      const ma = Return(x).bind(f);
      const mb = f(x);
      assert(ma.extract() === mb.extract());
    });

    it('Monad law 2', () => {
      const f = (n: number) => Return(n + 1);
      const x = 0;
      const ma = Return(x);
      const mb = ma.bind(Return);
      assert(ma.extract() === mb.extract());
    });

    it('Monad law 3', () => {
      const f = (n: number) => Return(n + 2);
      const g = (n: number) => Return(n * 3);
      const x = 1;
      const ma = Return(x)
        .bind(f)
        .bind(g);
      const mb = Return(x)
        .bind(n =>
          f(x)
            .bind(g));
      assert(ma.extract() === mb.extract());
    });

モナドであることが確認できました。

ついでに実行速度を計測しておきます。

    it('Just', function (done) {
      benchmark('Just', () => Just(0), done);
    });

    it('bind', function (done) {
      const just = Just(0);
      benchmark('bind', () => just.bind(n => Just(n)).extract(), done);
    });
Chrome 47.0.2526 (Windows 7 0.0.0) LOG: 'Maybe Just x 16,005,033 ops/sec ±4.19% (74 runs sampled)'
.
Firefox 43.0.0 (Windows 7 0.0.0) LOG: 'Maybe Just x 10,628,015 ops/sec ±7.73% (70 runs sampled)'
.
Chrome 47.0.2526 (Windows 7 0.0.0) LOG: 'Maybe bind x 2,352,541 ops/sec ±3.78% (74 runs sampled)'
.
Firefox 43.0.0 (Windows 7 0.0.0) LOG: 'Maybe bind x 2,776,510 ops/sec ±5.23% (74 runs sampled)'
.

十分な速さです。

さて、本題のEitherモナドを作ってみましょう。

export class Either<L, R> {
  private EITHER: [L, R];
  constructor(private thunk_?: () => Either<L, R>) {
  }
  public bind(f: (val: R) => Either<L, R>): Either<L, R>
  public bind<_R>(f: (val: R) => Either<L, _R>): Either<L, _R>
  public bind<_R>(f: (val: R) => Either<L, _R>): Either<L, _R> {
    return new Either<L, _R>(() => {
      const m = this.thunk_();
      switch (true) {
        case m instanceof Left: {
          return <Left<L>><any>m;
        }
        case m instanceof Right: {
          return f((<Right<R>>m).extract());
        }
        case m instanceof Either: {
          return m.bind<_R>(f);
        }
        default: {
          throw new TypeError(`Invalid monad value: ${m}`);
        }
      }
    });
  }
  public extract(): L | R
  public extract<_L>(transform: (left: L) => _L): _L | R
  public extract<_L>(transform?: (left: L) => _L): L | _L | R {
    return (<(transform: (left: L) => _L) => L | _L | R>this.thunk_().extract)(transform);
  }
}

export class Left<L> extends Either<L, any> {
  private EITHER_LEFT: L;
  constructor(private val_: L) {
    super();
  }
  public bind(f: (val: any) => Either<L, any>): Left<L> {
    return <any>this;
  }
  public extract(): L
  public extract<_L>(transform: (left: L) => _L): _L
  public extract<_L>(transform?: (left: L) => _L): L | _L {
    return transform ? transform(this.val_) : this.val_;
  }
}

export class Right<R> extends Either<any, R> {
  private EITHER_RIGHT: R;
  constructor(private val_: R) {
    super();
  }
  public bind<L>(f: (val: R) => Either<L, R>): Either<L, R>
  public bind<L, _R>(f: (val: R) => Either<L, _R>): Either<L, _R>
  public bind<L, _R>(f: (val: R) => Either<L, _R>): any {
    return new Either<L, R>(() => this).bind<_R>(f);
  }
  public extract(transform?: (left: any) => any): R {
    return this.val_;
  }
}
import {Either as Either_, Left as Left_, Right as Right_} from './either-impl';

export type Either<L, R> = Either_<L, R>;
export namespace Either {
  export type Left<L> = Left_<L>;
  export function Left<L>(val: L): Left<L> {
    return new Left_<L>(val);
  }
  export type Right<R> = Right_<R>;
  export function Right<R>(val: R): Right<R> {
    return new Right_<R>(val);
  }
  export const Return = Right;
}
export type Left<L> = Either.Left<L>;
export const Left = Either.Left;
export type Right<R> = Either.Right<R>;
export const Right = Either.Right;
export const Return = Either.Return;

基本動作

    it('Left', () => {
      const result = Return(0)
        .bind(n => Right(n + 1))
        .bind(n => Right(n + 1).bind(n => Left(`Left ${n}`)))
        .bind(n => Right(`Right ${n}`))
        .extract(l => `Left ${l}`);
      assert(result === 'Left Left 2');
    });

    it('Left nest', () => {
      const result = Return(Return(0))
        .bind(m => m.bind(n => Left(NaN)).bind(throwError))
        .bind(throwError)
        .extract(_ => 'Nothing');
      assert(result === 'Nothing');
    });

    it('Right', () => {
      const result = Return(0)
        .bind(n => Right(n + 1))
        .bind(n => Right(n + 1).bind(n => Right(`Right ${n}`)))
        .extract(l => `Left ${l}`);
      assert(result === 'Right 2');
    });

    it('Right nest', () => {
      const result = Return(Return(0))
        .bind(m => Right(m))
        .bind(m => m.bind(n => Right(n + 1)).bind(n => Right(`Right ${n}`)))
        .extract(_ => 'Nothing');
      assert(result === 'Right 1');
    });

モナド

    it('Monad law 1', () => {
      const f = (n: number) => Return(n + 1);
      const x = 0;
      const ma = Return(x).bind(f);
      const mb = f(x);
      assert(ma.extract() === mb.extract());
    });

    it('Monad law 2', () => {
      const f = (n: number) => Return(n + 1);
      const x = 0;
      const ma = Return(x);
      const mb = ma.bind(Return);
      assert(ma.extract() === mb.extract());
    });

    it('Monad law 3', () => {
      const f = (n: number) => Return(n + 2);
      const g = (n: number) => Return(n * 3);
      const x = 1;
      const ma = Return(x)
        .bind(f)
        .bind(g);
      const mb = Return(x)
        .bind(n =>
          f(x)
            .bind(g));
      assert(ma.extract() === mb.extract());
    });

ベンチマーク

    it('Right', function (done) {
      benchmark('Right', () => Right(0), done);
    });

    it('bind', function (done) {
      const right = Right(0);
      benchmark('bind', () => right.bind(n => Right(n)).extract(), done);
    });
Chrome 47.0.2526 (Windows 7 0.0.0) LOG: 'Either Right x 19,315,144 ops/sec ±4.97% (76 runs sampled)'
.
Firefox 43.0.0 (Windows 7 0.0.0) LOG: 'Either Right x 12,051,406 ops/sec ±5.15% (77 runs sampled)'
.
Chrome 47.0.2526 (Windows 7 0.0.0) LOG: 'Either bind x 2,131,294 ops/sec ±3.88% (77 runs sampled)'
.
Firefox 43.0.0 (Windows 7 0.0.0) LOG: 'Either bind x 2,634,425 ops/sec ±5.30% (77 runs sampled)'
.

MaybeモナドもEitherモナドJavaScriptで実用的な実装を作ることができます。

これらのモナドは以下のライブラリに含まれています。

github.com

追記

ライブラリではモナドが複数回評価されないようメモ化しました。

TypeScriptのコンパイルだけでBrowserifyを使わずにモジュールファイルを結合したIsomorphicなNPMのパッケージを作る

TypeScript1.8ではモジュールを結合して1つのファイルに出力できるようになり、これによりBrowserifyを使わずにIsomorphicなJavaScriptライブラリを作れるようになります。NPMのパッケージもNodeでもブラウザでもそのまま使える素敵なライブラリが簡単に作れます。これはもうTypeScriptを使うしかありませんね(バージョン1.8から)。

この記事はTypeScript アドベントカレンダー2015 8日目の記事です。

github.com

出力したモジュールファイルを1つのファイルに結合する機能がマージされました。

ただしこの機能はパッケージ名でネームスペースを集約できないようなのでライブラリ開発でそのまま使うことはできません。

パッケージ名でルートのファイルを作ることでネームスペースを区切れるようなのでそこだけ気をつけましょう。

// src/a.ts
export default 1;

// src/b.ts
import a from './a';
export var b = a + 1;

// mylib.ts
import a from './src/a';
import {b} from './src/b';
export default a + b;
console.log(a, b);

これを1つのファイルにコンパイルします。

$ tsc -t es5 -m amd --moduleResolution node --outFile bundle.js mylib.ts src/a.ts src/b.ts

次のように出力されます

// bundle.js
define("src/a", ["require", "exports"], function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.default = 1;
});
define("src/b", ["require", "exports", "src/a"], function (require, exports, a_1) {
    "use strict";
    exports.b = a_1.default + 1;
});
define("mylib", ["require", "exports", "src/a", "src/b"], function (require, exports, a_2, b_1) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.default = a_2.default + b_1.b;
    console.log(a_2.default, b_1.b);
});

このままでは動かないので先頭に以下のコードを追加してブラウザとNodeの両方で動くようにします(Node/Chrome/Firefox/IE9で動作確認済み)。

ソースコードでrequire, exports, moduleを触っているコードには対応していないのでes6 modulesだけ使ってください。

gist.github.com

define関数が定義されていなければブラウザでもNodeと同様に同期的に読み込まれグローバル変数に追加されます。

// node
$ node
> require('./bundle.js')
1 2
{ default: 3 }
// browser
> 1 2
mylib
> { default: 3 }

うまく動きました。

実際にこの方法で作ったクロスプラットフォームで動作するパッケージがこちらです。

github.com

TypeScript1.8がリリースされたらNPMのパッケージはTypeScriptでIsomorphicに作りましょう。

ヘキサゴナルアーキテクチャとレイヤードアーキテクチャの違いとマイクロサービス

ヘキサゴナルアーキテクチャレイヤードアーキテクチャのざっくりとした違いとヘキサゴナルアーキテクチャの利点を説明する。

ヘキサゴナルアーキテクチャは主に六角形で描かれる、多角形の全辺が外部とのインターフェイスであり、内部にさらにアプリケーションレイヤーやドメインコンテキストといった1つ以上のレイヤーを持つアーキテクチャである。 層構造はレイヤードアーキテクチャと同じであり、要はレイヤードアーキテクチャを最下層を軸に一回転させて全方位に対して層構造の性格が変わらないようにしただけである。

ヘキサゴナルアーキテクチャは実装においてはレイヤードアーキテクチャと同じだが、設計を行う上でいくつかの認知的効果がある。

複数インターフェイスを表現しやすい

レイヤードアーキテクチャではインターフェイスは直線または長方形1つのインターフェイスレイヤーで表現されるため複数インターフェイスを定義するにはこの直線や長方形を細切れに分割していかなければならず複数インターフェイスを表現しにくかった。

ヘキサゴナルアーキテクチャでは各面によって異なるインターフェイスを表現できるため複数インターフェイスを設計しやすくインターフェイス複数持つ設計に積極的になれる。

インターフェイスビジネスロジックの分離が促進される

レイヤードアーキテクチャではインターフェイスレイヤーにビジネスロジックなど他のレイヤーの責務が混入するしやすく異なるインターフェイスを追加する障害となっていた。

ヘキサゴナルアーキテクチャは全周囲からアクセスが行われることが視覚的に明白であるためサービスがどのインターフェイスにおいても等しく機能しなければならないという設計要件を効果的に認識させることができる。開発者は常にいずれかひとつの面だけでなくすべての面からの入力に対して等しく機能する内部レイヤーを作らなければならないというイメージを持って開発することになるため抽象化が促進されインターフェイスレイヤーが適切に構築されるようになる。

なお内部レイヤーについてはインターフェイスと同じく六角形で表現する例がよくあるが円形で表現することを勧める。内部レイヤーは基本的にすべてのインターフェイスに対して同じ共通のAPIを提供すべきであり、インターフェイスごとにAPIを変える設計というのはナンセンスだからである。

複数サービス間の連携を表現しやすい

レイヤードアーキテクチャは上下の方向の概念があるため複数アーキテクチャを連携のためにそのまま並べようとすると上下へアクセスの端点を接続できる配置を意識しなければならない煩雑さがある。これはサービス同士の関係がピラミッド型の厳格な階層構造である場合は都合がいいがネットワーク型である場合は向いていない。

ヘキサゴナルアーキテクチャは全周囲にアクセスの端点を持てるためネットワーク状のサービス連携を表現しやすい。

なお通信を行うレイヤーがインターフェイスであるかドメインまたはインフラであるかの差異を多角形の上下または左右で表現する例がよくあるが端点を外周のインターフェイスに持つか内部レイヤーに持つかで表現することを勧める。このような図は互いの位置関係を気にすることなく依存関係を表現することができるからである。たとえばサービスAの内部レイヤーとサービスBのインターフェイスが線で結ばれているならば位置関係に関係なくAはBに依存している。

マイクロサービスと相性がいい

上記のような複数サービス間の連携の表現しやすさはマイクロサービスの設計と相性がよい。マイクロサービスの連携を設計するときは見た目だけでもヘキサゴナルアーキテクチャで表したほうがいいだろう。実体は後から実装すればよい。

まとめ

要はレイヤードアーキテクチャを正しく実装するためのより正確で効果的な視覚表現がヘキサゴナルアーキテクチャである。レイヤードアーキテクチャと比べて特に変わったことをしているわけではない。