Haskellの差分リストはなんちゃって差分リストではないか?

Haskellの差分リストは一般に([1,2,3] ++) . ([4,5,6] ++)のようにセクションで示される。しかしこれは連結の際に左側のリストの要素をたどって末尾を見るので計算量がリストの長さに比例して増加しO(n)となる。一方CTMCP(コンピュータプログラミングの概念・技法・モデル)では要素をたどらず直接末尾を見るので計算量がリストの長さに比例して増加せずO(1)となる実装がOzにより示され、そしてこの効率性が差分リストの特徴とされている。Haskellも遅延評価の場合は連結コストが使用時のコストに同化されO(1)となるかもしれないが正格評価の場合は連結時に直ちにO(n)のコストを支払わなければならず、さらにこの状況は完全な正格評価でなくともデータが正格あるいは評価済みであるだけでも生じるため遅延評価の中でも無縁な話ではない。これは本当に差分リストとして説明してよいのだろうか?どうも過日のなんちゃってクイックソートと同じ種類の誤りを含んでいるように見える。

そういえばHaskellの差分リストについて気になることがあったのを思い出したのでアドカレのネタに書いてみた。


議論の結果、Haskellの差分リストがO(n)で非効率となる状況は連結演算まで正格となる状況まで限定できそうであるという結論になった。

Haskellの差分リストはなんちゃって差分リストではないか? : haskell_jp

AsyncIteratorとPromiseによるObservablePromise抽象

ObservablePromiseはAsyncIterableインターフェイスを実装することで途中状態を取得可能にしたPromiseである。ObservablePromiseの最大の利点は非同期の途中状態と最終状態という意味的に異なる状態の分離をfor-awaitとasync/awaitという言語抽象レベルで行うことで標準的な形でこれを区別して扱えることであり、その内部表現もまた言語抽象であるAsync generatorで標準にのっとり簡潔に記述できる。

ObservablePromise抽象はコルーチンと親和性が高いためここではObservablePromiseをコルーチンとして実装した。ただしコルーチンの操作的基本機能であるsuspend/resumeはJavaScriptでは実用性が低いためプロパティを通してポートオブジェクトの形で追加的に提供する*1。ObservablePromiseは次のような表現を可能にする。なおこのコルーチンはCancelableでもあるためより正確にはCancelableObservablePromiseであり、単なるCancelablePromiseとしても有用性が高い。また基底のPromiseには同期Promiseを使用しておりコルーチンの生死判定とその事後処理は遅延なく同期的に行われる。

    it('terminate', done => {
      let cnt = 0;
      const co = new Coroutine(async function* () {
        assert(cnt === 0 && ++cnt);
        return 0;
      });
      co.finally(() => {
        assert(cnt === 1 && ++cnt);
      });
      co[Coroutine.terminator]();
      assert(cnt === 2 && ++cnt);
      co[Coroutine.terminator](1);
      co.catch(done);
    });

    it('iterate', async () => {
      let cnt = 0;
      const co = new Coroutine<number, number>(async function* () {
        assert(++cnt === 1);
        assert(undefined === (yield Promise.resolve(2)));
        assert(undefined === (yield Promise.resolve(3)));
        await wait(100);
        assert(undefined === (yield Promise.resolve(4)));
        return Promise.resolve(5);
      });
      assert(cnt === 1);
      for await (const n of co) {
        assert(n === ++cnt);
      }
      for await (const _ of co) {
        assert(false);
      }
      assert(await co === ++cnt);
      assert(cnt === 5);
    });

https://github.com/falsandtru/spica/blob/master/src/coroutine.test.ts

XMLHttpRequestはObservablePromiseで表現することで非常に簡潔になる。

    it('basic', async () => {
      const co = cofetch('');
      for await (const ev of co) {
        assert(ev instanceof ProgressEvent);
        assert(['loadstart', 'progress', 'loadend'].includes(ev.type));
      }
      assert(await co instanceof XMLHttpRequest);
    });

https://github.com/falsandtru/spica/blob/master/src/cofetch.test.ts

このようにObservablePromiseは優れた抽象表現でありその標準性から一般に推奨できるものである。ただしNode.jsが非同期APIにPromiseを採用しなかったようにパフォーマンス上の問題が生じる場合はこの限りではない。また2018年12月時点ではEdgeでAsyncIteratorが未実装であるためブラウザではもうしばらく待つ必要がある。そのほか、自身はコルーチンの管理にSupervisorを使用してプロセス管理の煩雑さを低減している。そしてこのSupervisorもまたコルーチンである。

*1:このコルーチンは入力のキューのサイズが0のときは入力を取らず自動で非同期イテレーションを回し、1以上のときは入力の都度イテレーションを回すことで自動手動両方のイテレーション方法に対応している。

テキストエリアとブラウザベーステキストエディタの文字数限界

Chrome v70でエディタへの入力にテキストエリアを使用する標準的ケースの場合、ASCII文字の数十万文字程度でテキストエリアへの入力に遅延が生じ始める(遅延の大きさは後続文字数に比例し、よって入力済み文字列の先頭で最大となる)。Firefoxでは入力遅延はないように見えるが入力が数十万文字を超えると現実的な時間でこれを描画できない。このため現時点でブラウザ上に実装するテキストエディタが現実的に扱える文字数は10万文字程度までであり、エディタおよびレンダラの性能はこの文字数まで実用的な速度で動作すればよく、これを超える文字数への対応はブラウザ(テキストエリア)がボトルネックとなるため無意味である。

例として以下のMarkdownエディタは100万文字でも十分な速度*1で動作するがテキストエリアそのものの入力遅延によりこの文字数では実用に耐えない*2

https://falsandtru.github.io/securemark/

*1:100万文字を1分以内、追加入力を1秒以下で描画、ただし入力文字列は描画において画像等追加の描画を必要としないものとする。

*2:この文字数では入力中引っ掛かりを感じることがあるかもしれないが解消可能であるもののこの文字数に対応する意味がないためそのままにしている。

TypeScriptで型レベル関数とRequired/DeepRequired型を作って型レベルプログラミング

TypeScript 2.8で追加予定のConditional型とinfer prefixが開発版でリリースされたので型レベル関数と念願のRequired/DeepRequired型を作って型レベルプログラミングを可能にする。

型レベル関数

Conditional型はUnion型を展開するためEq<false, boolean>の結果はその非決定性からbooleanとなる。決定的な結果を返す関数はboolean型にのみ用意しておりDEq<false, boolean>undefinedとなる。

type Falsy = undefined | false | 0 | '' | null | void;
type Function = (...args: any[]) => any;
type Class = new (...args: any[]) => any;

export type Not<T extends boolean> =
  T extends true ? false :
  T extends false ? true :
  never;
export type And<T, U> = T extends Falsy ? T : U;
export type Or<T, U> = T extends Falsy ? U : T;
export type Eq<T, U> = T extends U ? U extends T ? true : false : false;
export type If<S, T, U> = S extends Falsy ? U : T;
export type Case<T extends keyof U, U extends {}> = U[T];

export type DEq<T extends valueof<NondeterminateTypeMap>, U extends valueof<NondeterminateTypeMap>> =
  Determine<T> extends undefined ? undefined :
  Determine<U> extends undefined ? undefined :
  Eq<T, U>;
type Determine<T extends valueof<NondeterminateTypeMap>> =
  valueof<NondeterminateTypeMap> extends T ? undefined :
  T;
interface NondeterminateTypeMap {
  boolean: boolean;
}

export type keyof<T, V = any> = { [P in keyof T]: If<Eq<T[P], V>, P, never>; }[keyof T];
export type valueof<T, K = string> = { [P in keyof T]: P extends K ? T[P] : never; }[keyof T];
  describe('Eq', () => {
    it('', () => {
      assert((): true => true as Eq<true, true>);
      assert((): false => true as Eq<true, false>);
      assert((): false => true as Eq<false, true>);
      assert((): true => true as Eq<false, false>);
      assert((): Eq<true, boolean> => true as boolean);
      assert((): Eq<false, boolean> => true as boolean);
      assert((): Eq<boolean, true> => true as boolean);
      assert((): Eq<boolean, false> => true as boolean);
      assert((): Eq<boolean, boolean> => true as boolean);
      assert((): true => true as Eq<0, 0>);
      assert((): false => true as Eq<0, number>);
      assert((): true => true as Eq<number, number>);
      assert((): true => true as Eq<undefined, undefined>);
      assert((): false => true as Eq<void, undefined>);
      assert((): false => true as Eq<void, null>);
      assert((): false => true as Eq<void, undefined | null>);
      assert((): true => true as Eq<void, void>);
    });

  });

  describe('If', () => {
    it('', () => {
      assert((): 1 => 0 as If<true | 1, 1, 0>);
      assert((): 0 => 0 as If<false | 0, 1, 0>);
    });

  });

  describe('Case', () => {
    it('', () => {
      assert((): 1 => 0 as Case<'0', [1]>);
      assert((): 1 => 0 as Case<'0', { 0: 1 }>);
      assert((): number => 0 as Case<'1', { 0: 1, [otherwise: string]: number }>);
    });

  });

  describe('DEq', () => {
    it('', () => {
      assert((): true => true as DEq<true, true>);
      assert((): false => true as DEq<true, false>);
      assert((): false => true as DEq<false, true>);
      assert((): true => true as DEq<false, false>);
      assert((): undefined => undefined as DEq<true, boolean>);
      assert((): undefined => undefined as DEq<false, boolean>);
      assert((): undefined => undefined as DEq<boolean, true>);
      assert((): undefined => undefined as DEq<boolean, false>);
      assert((): undefined => undefined as DEq<boolean, boolean>);
    });

  });

Requiredなど

Deep系は除外するオブジェクトを指定できるので必須と任意を組み合わせたりDOM要素などの組み込みオブジェクトを持つオブジェクトにも使用できる。

export type Partial<T> =
  T extends object
    ? { [P in keyof T]?: T[P]; }
    : T;
export type DeepPartial<T, U extends object | undefined = undefined> =
  T extends object
    ? { [P in keyof T]?: NonNullable<T[P]> extends NonNullable<U | Function | Class> ? T[P] : DeepPartial<T[P], U>; }
    : T;
type Purify<T extends string> = { [P in T]: P; }[T];
export type Required<T> =
  T extends object
    ? { [P in Purify<keyof T>]: NonNullable<T[P]>; }
    : T;
export type DeepRequired<T, U extends object | undefined = undefined> =
  T extends object
    ? { [P in Purify<keyof T>]: NonNullable<T[P]> extends NonNullable<U | Function | Class> ? NonNullable<T[P]> : DeepRequired<NonNullable<T[P]>, U>; }
    : T;
export type Readonly<T> =
  T extends object
    ? { readonly [P in keyof T]: T[P]; }
    : T;
export type DeepReadonly<T, U extends object | undefined = undefined> =
  T extends object
    ? { readonly [P in keyof T]: NonNullable<T[P]> extends NonNullable<U | Function | Class> ? T[P] : DeepReadonly<T[P], U>; }
    : T;
  describe('Partial', () => {
    it('', () => {
      type R = { a: number; b: { c: string; }; d: () => 0; e: new () => object };
      type P = { a?: number; b?: { c: string; }; d?: () => 0; e?: new () => object };
      assert((): P => ({}) as Partial<R>);
      assert((): Partial<R> => ({}) as P);
      assert((): P => ({}) as Partial<Required<R>>);
      assert((): Partial<Required<R>> => ({}) as P);
    });

  });

  describe('DeepPartial', () => {
    it('', () => {
      type R = { a: number; b: { c: string; d: () => 0; e: new () => object }; };
      type P = { a?: number; b?: { c?: string; d?: () => 0; e?: new () => object }; };
      assert((): P => ({}) as DeepPartial<R>);
      assert((): DeepPartial<R> => ({}) as P);
      assert((): P => ({}) as DeepPartial<R>);
      assert((): DeepPartial<R> => ({}) as P);
      assert((): Partial<R> => ({}) as DeepPartial<R, R['b']>);
      assert((): DeepPartial<R, R['b']> => ({}) as Required<R>);
    });

  });

  describe('Required', () => {
    it('', () => {
      type R = { a: number; b: { c?: string; }; d: () => 0; e: new () => object };
      type P = { a?: number; b?: { c?: string; }; d?: () => 0; e?: new () => object };
      assert((): R => ({}) as Required<P>);
      assert((): Required<P> => ({}) as R);
      assert((): R => ({}) as Required<Partial<R>>);
      assert((): Required<Partial<R>> => ({}) as R);
    });

  });

  describe('DeepRequired', () => {
    it('', () => {
      type R = { a: number; b: { c: string; d: () => 0; e: new () => object }; };
      type P = { a?: number; b?: { c?: string; d?: () => 0; e?: new () => object }; };
      assert((): R => ({}) as DeepRequired<P>);
      assert((): DeepRequired<P> => ({}) as R);
      assert((): R => ({}) as DeepRequired<DeepPartial<R>>);
      assert((): DeepRequired<DeepPartial<R>> => ({}) as R);
      assert((): Required<P> => ({}) as DeepRequired<P, P['b']>);
      assert((): DeepRequired<P, P['b']> => ({}) as Required<P>);
    });

  });

  describe('Readonly', () => {
    it('', () => {
      type I = { readonly a?: number; readonly b: { c: string; }; readonly d: () => 0; readonly e: new () => object };
      type M = { a?: number; b: { c: string; }; d: () => 0; e: new () => object };
      assert((): I => ({}) as Readonly<M>);
      assert((): Readonly<M> => ({}) as I);
    });

  });

  describe('DeepReadonly', () => {
    it('', () => {
      type I = { readonly a?: number; readonly b: { readonly c: string; readonly d: () => 0; readonly e: new () => object }; };
      type M = { a?: number; b: { c: string; d: () => 0; e: new () => object }; };
      assert((): I => ({}) as DeepReadonly<M>);
      assert((): DeepReadonly<M> => ({}) as I);
      assert((): Readonly<M> => ({}) as DeepReadonly<M, M['b']>);
      assert((): DeepReadonly<M, M['b']> => ({}) as Readonly<M>);
    });

  });

型レベルプログラミング

非決定性計算がはかどりそうな気がするがお題が浮かばなかった。

https://github.com/falsandtru/spica/blob/master/src/lib/type.ts https://github.com/falsandtru/spica/blob/master/src/lib/type.test.ts

ポストjQueryの要件

jQueryからの脱却が叫ばれる所以はクエリによりカプセル化の境界を簡単に逸脱する局所性のなさ、また要素やコンポーネント間の連携を求めるにはクエリでは対象の存在も同一性も不確実すぎるなどの理由から大規模なコードベースを構築できないためです。これを克服しようとするとクエリを取り除かなければなりませんが取り除いただけでは使い物にならないのでクエリの代わりになるDOM制御方法を用意しなければなりません。加えてjQueryはDOMの構築も苦手なのでここも補ってやらねばなりません。

これをどのように解決するかというと、静的型付き言語でのみ利用できるアプローチですがDOM構造を静的に型付けし、型をたどってのみ目的とする要素を操作できるようにする方法が考えられます。正しいパスは型によって示され、間違ったパスは型によってエラーとなり、対象の存在と同一性が保証されます。TypeScriptでは以下のような定義と操作が可能です。構造には文字列、配列、任意のフィールドを持つ構造体のいずれかを選択できます。

import TypedHTML from 'typed-dom';

const component = TypedHTML.article({ id: 'id' }, {
  style: TypedHTML.style(`$scope ul { width: 100px; }`),
  title: TypedHTML.h1(`title` as string),
  content: TypedHTML.ul([
    TypedHTML.li(`item` as string),
    TypedHTML.li(`item`),
  ])
});
// inspect
component.element.outerHTML; // '<article id="id"><style>#id ul { width: 100px; }</style><h1>title</h1><ul><li>item</li><li>item</li></ul></article>'
component.children.title.element.outerHTML; // '<h1>title</h1>'
component.children.title.children; // 'title'
component.children.content.element.outerHTML; // '<ul><li>item</li><li>item</li></ul>'
component.children.content.children[0].children; // 'item'

// update
// - text
component.children.title.children = 'Title';
component.children.title.element.outerHTML; // '<h1>Title</h1>'

// - struct
component.children.content.children = [
  TypedHTML.li('Item')
];
component.children.content.element.outerHTML; // '<ul><li>Item</li></ul>'

// - TypedHTML
component.children.title = TypedHTML.h1('Title!');
component.children.content = TypedHTML.ul([
  TypedHTML.li('Item!')
]);
component.element.outerHTML; // '<article id="id"><style>#id ul { width: 100px; }</style><h1>Title!</h1><ul><li>Item!</li></ul></article>'

HTML以下のDOMを完全に静的型付けすることも可能です。ただし実際には末端の要素へのパスが長くなりすぎるため適切な単位で抽象化することになります。

ポストjQueryにはこのようにクエリを除去しカプセル化を守るライブラリが求められます。なお仮想DOMについてはDOMをコンポーネントなど大きな単位で更新するコストを下げるための技術なので最小コストで更新できるのであれば不要です。

github.com

TypeScriptでIDなどの特別な数値や文字列を型安全な専用の属性付きプリミティブ型にする

TypeScriptでIDやエンコーディングなど特別なセマンティクスを持つ値にプリミティブ型を維持したまま個別の属性を与えることで型安全な専用の値を作る。言語の型システムをどのように機能させているかについての説明は省略する。

IDは次のように作れる。

namespace Identifier {
  declare class Event<T extends string> {
    private IDENTITY: T;
  }
  export type Number = Event<any> & number;

  export type Id = Event<'Id'> & number;
}

export type EventId = Identifier.Id;

export function makeEventId(id: Identifier.Number): void
export function makeEventId(id: number): EventId
export function makeEventId(id: number): EventId {
  assert(Number.isFinite(id));
  assert(Math.floor(id) === id);
  assert(id >= 0);
  return <EventId>id;
}

IDのように特定のセマンティクス内で識別する値はIDという属性でなくセマンティクスを構成するドメインを主体として型を設計する。ゆえに型はDomain<Attribute>となりAttribute<Domain>とはならない。

エンコーディングは次のように表現できる。

// ./attribute/encode.ts
export declare class Encoded {
  private ENCODE;
}
// ./attribute/normalize.ts
export declare class Normalized {
  private NORMALIZE;
}
// ./domain/url.ts
import { Encoded } from '../attribute/encode';
import { Normalized } from '../attribute/normalize';

namespace Identifier {
  declare class Url<T> {
    private IDENTITY: T;
  }

  export type URL<T> = Url<T> & string;
}

type Url<T> = Identifier.URL<T>;


// https://www.ietf.org/rfc/rfc3986.txt

export type StandardUrl = Url<Normalized & Encoded>;

export function standardizeUrl(url: Url<any>): void
export function standardizeUrl(url: string): StandardUrl
export function standardizeUrl(url: string): StandardUrl {
  return encode(normalize(url));
}


type EncodedUrl = Url<Encoded>;

function encode(url: EncodedUrl): void
function encode<T>(url: Url<T>): Url<T & Encoded>
function encode(url: string): EncodedUrl
function encode(url: string): EncodedUrl {
  return <EncodedUrl>url
    // Trim
    .trim()
    // Percent-encoding
    .replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]?|[\uDC00-\uDFFF]/g, str =>
      str.length === 2
        ? str
        : '')
    .replace(/%(?![0-9A-F]{2})|[^%\[\]]+/ig, encodeURI)
    .replace(/\?[^#]+/, query =>
      '?' +
      query.slice(1)
        .replace(/%[0-9A-F]{2}|[^=&]/ig, str =>
          str.length < 3
            ? encodeURIComponent(str)
            : str))
    .replace(/#.+/, fragment =>
      '#' +
      fragment.slice(1)
        .replace(/%[0-9A-F]{2}|./ig, str =>
          str.length < 3
            ? encodeURIComponent(str)
            : str))
    .replace(/%[0-9A-F]{2}/ig, str => str.toUpperCase());
}


type NormalizedUrl = Url<Normalized>;

function normalize(url: string): NormalizedUrl
function normalize(url: string): NormalizedUrl {
  // Absolute path
  const parser = document.createElement('a');
  parser.href = url || location.href;
  return <NormalizedUrl>parser.href
    // Remove the default port
    .replace(/^([^:/?#]+:\/\/[^/?#]*?):(?:80)?(?=$|[/?#])/, '$1')
    // Fill the root path
    .replace(/^([^:/?#]+:\/\/[^/?#]*)\/?/, '$1/')
    // Use uppercase letters within percent-encoding triplets
    .replace(/%[0-9A-F]{2}/ig, str => str.toUpperCase());
}

Urlとその正規化は公開・一般化されたドメイン横断の知識であるためここではUrlを属性でなくドメインとして定義した。移動距離が長い場合は転送用のオブジェクトで包んでもいいだろう。

これらの手法は以下のライブラリで使われており、具体的な組み込み方と使用方法を確認できる。

github.com github.com

IndexedDBの安全な永続化リソース管理戦略 ~ 有効期限と容量制限

IndexedDBはキャッシュによる通信の削減の用途に非常に魅力的だが、クライアント/ユーザー側にサーバー/サービス提供者側から管理・修正不能な状態を作るリスクがあり、最悪の場合クライアント/ユーザー側でキャッシュを削除しない限りサービスが利用不能となる可能性がある。これはサービスの規模および継続性に比例して許容できないリスクとなる。

このためIndexedDBをプロダクションで使うためには開発者から認識されず、または無視される管理外データを自動的に削除する機能が不可欠となる。これは通常、有効期限の機能追加により解決できる。また容量(件数)制限も非常に有用である。有効期限と容量制限は管理外データ、ひいてはこれを管理する問題のサイズに上限を与え、管理可能なサイズに縮小する。

永続データの終末処理はこれまで開発者がすべてのデータを把握して明示的に削除する必要があったが、使わないデータも古いデータも自動的に消えるなら管理は不要となり、終末処理はただ管理を放棄するだけよくなる。このような運用環境では開発者は任意のバージョンのデータだけ考慮すれば済み、それ以外は放置か削除で足りるため永続化リソースの利用コストが大幅に削減される。

このように有効期限と容量制限(超過データの自動削除)の制約を与えることでサーバー管理下にない永続化リソースも相当程度安全かつ低コストで利用できるものとなる。複雑なバージョン管理に由来するバリデーションやマイグレーションの失敗もブラウザの再起動によりバージョンを最新にそろえればほとんど解決するので、サポートも通常のバグ対応の範疇に収まり容易となる。ここまでくるともはや永続化リソースのために必要な特別な対応は任意のバージョン間でのマイグレーション名前空間の競合の回避程度しかなく、永続化リソースを使用するハードルは非常に低い。

IndexedDBはサーバー管理下にない永続化リソースの制御の難しさから採用しづらいものであったが、有効期限と容量制限の制約を自律させることで採用を検討できる安全性が得られる。以下のライブラリはこの2つの制約を実装したものである。

github.com