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に依存している。

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

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

まとめ

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

TypeScriptで形式的証明・交差型編 ~ 状態付きの型による高信頼領域の構築

TypeScriptで交差型を使って静的型に状態的な型を動的に付与していきます。 これによりサニタイズ済/未サニタイズエンコード済/未エンコード、不変/可変、通常文脈/エラー文脈、Truthy/Falsyといった状態を複合的に扱えるようになります。 さっそく試してみましょう。

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

サニタイズ済みである文字列のみを受け取る型を作ってみます。

declare class Sanitized {
    private id;
}
function sanitize<T>(data: T): Sanitized&T {
    return <Sanitized&T>data; // any processing
}

var plain = '';
var secure: Sanitized&string = plain; // type error
var secure: Sanitized&string = sanitize(plain); // ok

たったこれだけです。たったこれだけで信頼境界の内側と外側を分離できるのです。もはやただの文字列は安全な文字列を要求する型に入れることができなくなりました。

見た目が通常の型宣言とよく似ているためアノテーションとしても利用できます。それでいて、この手法は型のみで実現され実装に影響を及ぼさないためまったく何のオーバーヘッドもありません。

信頼境界を越えるときに安全な文字列に変換し、境界内では安全な文字列だけ使うように制限すればプログラムが確実に安全なデータだけを使っていることが保証されるようになります。

苦労して経路を網羅しなくても受け取り側が型を見て不正なデータを拒否するので、境界内が要件を満たさない不正なデータで汚染されることはありません。

信頼境界内は常に安全なデータだけで満たされているクリーンな領域となります。

このような状態付きの型を定義する方法は他にもいくつかありますが、ここで紹介した交差型を使用した方法はその中でも汎用性が高く開発者の負担が小さいものです。今日からでも使い始められるでしょう。

上記のコードは最低限の機能しかないので、以下の条件を追加してもう少し実用的にしてみます。

  • 二重に状態を付与しようとした場合はvoidとなる。
export declare class Sanitized {
    private SANITIZED;
}

export function type<T>(target: Sanitized&T): void
export function type<T>(target: T): Sanitized&T
export function type<T>(target: T): Sanitized&T {
    return <Sanitized&T>target;
}

export function extract<T>(target: Sanitized&T): T {
    return <T>target;
}

プリミティブ型の次はデータ型にも対応できるようオブジェクトに状態を持たせてみましょう。 すいません動くコードロストして再実装できなくてロストテクノロジーになりました。以下のコード実装できた方いたら教えてください(泣)。 データ型については信頼できるデータのみを保持する信頼境界内用の公称型を用意して信頼性を確保してください。

declare class Sanitized {
    private id;
}
function sanitize<T>(data: T): Sanitized&T {
    return <Sanitized&T>data; // any processing
}

class Data<T> {
    constructor(public value: T) {
    }
    public sanitize(this: Data<Sanitized>): void
    public sanitize(): Data<Sanitized&T>
    public sanitize(): Data<Sanitized&T> {
        return new Data(sanitize(this.value));
    }
}

var plain = new Data('');
var secure: Data<Sanitized&string> = plain; // type error
var secure: Data<Sanitized&string> = plain.sanitize(); // ok
var secure: Data<Sanitized&string> = plain.sanitize().sanitize(); // type error

これまでは信頼境界内の信頼性を境界通過時の検査のみで担保・依存し、ひとたび境界内に入ればあとはフリーパスになって不正なデータが混入しても検出できないという状態に甘んじていたこともありましたが、型に状態を持てるようにすることで要求される状態を持たない値が境界内で誤って使われようとしても要件を満たせず拒否される信頼性の高い領域を構築できます。

これまで境界線という線で確保していた信頼性と安全性を境界内の空間全体というより高い次元で確保できるようになったのです。より応用的には個々のデータのうち特に信頼性を求める場合は専用に公称型のデータ型を用意することでさらに確実な操作を行い、それ以外のデータは状態の要件により信頼性を確保するといったアプローチが望ましいでしょう。

今回の内容は単純な状態遷移をモデルへ部分的に適用しただけなので形式的証明と呼べるほどのものではありませんでしたが*1、実装に高水準の信頼性を与える方法として形式的証明と同様非常に効果的なものです。

外部と通信を行うアプリケーションは規模の大小にかかわらずサニタイズと正規化を厳しく要求されるため本稿で紹介したように形式的に信頼性を保証する方法が非常に重要となってきますので、これを期にぜひ自身のプロダクトの信頼性と品質の向上に取り組んでみてください。

TypeScriptアドベントカレンダー2015、需要と余裕があれば次回は状態遷移モデルの形式的証明を行う並行処理および並列処理の形式的証明(状態遷移編)です。予定は未定。

*1:形式的証明は未証明部分で破綻していては意味がないので原則として証明対象の全機能を証明します

TypeScriptで形式的証明・直和型編 ~ じゃんけんゲーム最強トーナメント

TypeScriptで形式的証明を取り入れたプログラミングを実践します。

形式的証明とは、仕様またはモデルが数理論理学的に正しいものであり実装可能であることの形式手法による証明です。 本稿では形式手法のうち、型システムを利用した軽量形式手法による形式的証明を扱います。

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

TypeScriptアドベントカレンダー2015!!

f:id:falsandtru:20151130044653j:plain

去年あれだけいた戦士たちはどこへ行ったのか…

さて、形式的証明を実感するために1つのコードモジュールを作ってみましょう。

じゃんけんの結果をグー・チョキ・パーの3つの型を使って型推論だけで計算し、不正な入力を実行前に検出する、形式的妥当性の証明されたコードモジュールです。

まず前提となるグー・チョキ・パーの3つの型を作ります。

class Gu {
    private identity;
}
class Choki {
    private identity;
}
class Pa {
    private identity;
}

TypeScriptの型は構造的部分型だから型を区別できないと思っていましたか? TypeScriptには型を一意に識別する方法も用意されており、構造的部分型と公称的部分型の両方をサポートしています。

アクセシビリティ修飾子にprivateまたはprotectedを指定するとそのメンバーが他で定義された同名のメンバーに代入できない一意な識別子となります。

これによりTypeScriptでも公称型または公称的部分型である任意の代数的データ型とその直和型を作ることができるのです(数学的には共通部分のない型同士に限られますが実用上は気にする必要はないでしょう)。

また、TypeScriptのUnion typesは直和型そのものではありませんが直和型としての使い方もできるのでTypeScriptでも直和型の概念に基づいたプログラミングが可能です。

次にグーとチョキならグーが勝つといった論理(規則・仕様)を作ります。

interface Rule<T, U> {
  (a: T, b: U): T;
  (b: U, a: T): T;
}
interface Fight<T, U, V> extends Rule<T, U>, Rule<U, V>, Rule<V, T> {
}
var fight: Fight<Gu, Choki, Pa> = (a: Options, b: Options): any => void 0;
type Options = Gu|Choki|Pa;

最後にこれらの仕様の妥当性確認を行うバリデーションコードを書いて正しく機能しているか確認します。

<Gu>fight(new Gu, new Choki);
<Pa>fight(new Gu, new Pa);
...

これで完成です。さっそく試してみましょう。

var correctWinner: Gu = fight(new Gu, new Choki);
var incorrectWinner: Choki|Pa = fight(new Gu, new Choki); // type error

ちゃんと動いてますね。型だけで結果を得られるので問題によってはアルゴリズムを実装せずに正しい答えを得ることすら可能です。

var winner: Gu = fight(new Gu, new Choki) || new Gu;

型を見てから結果をハードコーディング余裕でした。テストでカンニングして途中計算を書かずに答えだけ書くようなものですね。

でもこれだけではつまらないのでもっとサイズを大きくしてみましょう。じゃんけんトーナメントを開催してみます。

グー・チョキ・パーの各インスタンスが個々の選手(の最初の手)となります。選手を区別したい場合はコンストラクタが初期化パラメータを受け取るようにして選手名などで選手を区別できるようにしましょう。このように実体を区別する必要があるときはさっきのようなずるはできません。

// 2回戦
fight(
    fight(new Gu, new Choki),
    fight(new Choki, new Pa)
); // Gu
// 3回戦
fight(
    fight(
        fight(new Gu, new Choki),
        fight(new Choki, new Pa)
    ),
    fight(
        fight(new Gu, new Pa),
        fight(new Choki, new Gu)
    )
); // Pa

問題なくトーナメントが成立することが証明されていますね。

とはいえ…

fight(
    fight(new Gu, new Pa),
    fight(new Pa, new Gu)
); // type error

Oops! エラーになりました。ドローになり勝ち負けが決まらない場合を考慮していなかったのです。

形式的証明を行っても非網羅的定義までフォローしてくれるとは限りません。Haskellなど一部のプログラミング言語は定義の網羅性を検証してくれますが状態遷移がすべての組み合わせにおいて破綻なく循環することまでは検証してくれません。形式的証明用の言語やツールであればおおむね網羅的な検証を実施する機能を備えていますが、通常のプログラミング言語の型システムを利用するだけではそうした本格的な検証までは行えないのです。あくまでソースコード中で具体的に示された組み合わせの中での検証となり、入力する型を誤った実装や実装不可能な仕様やモデルを作らないための検証です。また、いずれにしても根本的定義ミスには無力なので過信は禁物です。

しかし、トーナメントに欠陥があり成立しないことは事前に判明しました。それも実装アルゴリズムに関係なくテストケースの範囲外であってもです(形式的に正しいことを確認する妥当性確認は行いましたが、実装の正しさを検証する、通常のテストに該当する正当性検証は行っていません。なにしろ実装が空ですから。)。トーナメント中に発覚して興行を中止せざるをえなくなり、大ブーイングを浴びながら出資者からの法的責任追及と集団訴訟に怯えるよりよほどましです。これこそが形式手法と形式的証明の威力です。この欠陥の判明を受けてトーナメントを成立させるためにトーナメントが不成立となる組み合わせを認めないか、ドローの解決方法を定義するかは主催者しだいです。

以上のように、このケースでは形式的証明を導入したことで大きな成果を上げることができました。定義と運用の範囲内で動作可能であることが数理論理学的に証明され、逆に不可能である場合はそれを検出してコンパイルの段階で型エラーとして出力し、期待通り正しく機能したわけです。

宣言的で簡潔な型による抽象化された検証と証明は、アルゴリズムの実装可能性という尺度からソースコード上の仕様ないしモデルの評価を可能にし*1、手続き的になりやすく複雑な実装アルゴリズムの部分的テストよりも高水準な信頼性の指標となります*2

静的型付き言語は静的型により動的型付き言語よりも早い段階で実装の正しさを検査して品質と信頼性を高めてきましたが、形式的証明はさらに早く、実装前の段階で抽象化された仕様やモデルを検証しているのです。

このように形式的証明はコードの品質と信頼性の向上に大きく役立つものです。毎年新しいフレームワークを覚えては使い捨てるより形式手法を1つ覚えるほうがよほどにスキルとコード品質が向上します。形式的証明はこのさき10年でも20年でも生きつづける安定した技術分野です。みなさんもぜひ使ってみてください。

TypeScriptアドベントカレンダー2015、次回は交差型を使って型に状態を動的に付与する形式的証明(交差型編)です。お楽しみに。

本稿に掲載したコードは以下のリンク先で形式的動作を確認できます。

じゃんけんゲーム

*1:副作用だらけの言語でも型の世界では形式的証明に適した純粋関数プログラミングを実現できるのです。

*2:形式的証明は入出力の型以上の実装の正しさをテストしないので実装のテストも依然として必要です。