余白

https://blog.lacolaco.net/ に移転しました

RxJSによるWeb Workerの抽象化 2つのアプローチ

この記事では、RxJS を使ったWeb Workerの抽象化を試みます。 なお、記事中で Web WorkerあるいはWorkerと言ったときに指すのは new Worker() で作成する Dedicated Workerのみで、Shared WorkerやService Workerなどは対象外です。

なぜWeb Worker?

Web Workerを使うのに2つの目的があります。ひとつはoff-the-main-threadとよく言われる、UIメインスレッドとは別のWorkerスレッドで並行処理をおこなうことによるパフォーマンス改善です。 そしてもうひとつは、仕様がドラフト段階にある ES ModulesのWorker対応 を利用した Module Worker によるコード分割です。

https://html.spec.whatwg.org/multipage/workers.html#module-worker-example

ES ModulesのWorker対応は、現在Chromiumではフラグ付きでサポートされています。

www.chromestatus.com

Module Workerでは次のようなコードで type: 'module' を指定すると、コンストラクタに指定したパスをES Moduleとして読み込めます。 さらにWorkerスクリプト内でもES Moduleのコンテキストで他のモジュールをimport/export文を使えるようになります。

const worker = new Worker('./worker.mjs', { type: 'module' });

もちろんChromeですらまだ普通には使えない機能なので、今Module Workerを使うためには小細工が必要です。 webpackを使っている場合は、GoogleChromeチームが開発している WorkerPlugin を使うのが便利です。

github.com

WorkerPluginは type: 'module' オプションでModule Workerを作成しているコードを発見すると、 呼び出されているファイルをwebpackのCode Splitting機能で別バンドルに分割しながら、type: 'module' オプションを除去してくれます。

webpack.js.org

つまり、このプラグインさえ入れておけば、ES Moduleベースで書かれたファイルをModule Workerとして呼び出し、webpackのビルド後にはWorkerごとにバンドルが自動で分割されている、という状態になります。 多くの場合、Workerで実行したい処理というのはページの初期化時に必要なものではないでしょう。 たいていはユーザーインタラクションや何かのイベントを受けて実行される非同期的なジョブです。 そのような処理は遅延読み込みとWorkerの両方と相性がよいので、Module Workerはページの初期読み込みに必要なバンドルサイズを少なくしながらメインスレッド の負荷も下げられるまさに一石二鳥です。

RxJSによる抽象化

WorkerはpostMessage/ommessageによって他のスレッドとコミュニケーションします。 このイベント駆動の仕組みは、RxJSのSubjectモデルとよく似ています。 Workerそのものでは拡張性に乏しいですが、Subjectで抽象化することでRxJSのオペレーターを使ったデータ加工や、RxJSと連携できる他のJavaScriptライブラリなどとのコミュニケーションも容易になります。 そしてRxJSは元来が非同期処理を扱うためのものですから、そのイベントの由来が同じスレッドかWorkerスレッドかは気にせず同じ非同期の枠で考えられます。 このことからも、Workerによる別スレッドでの処理とそのイベント購読はRxJSでうまく抽象化できるのではないかと考えています。

f:id:lacolaco:20190326141608p:plain

アプローチ 1. Worker as a Subject

まずひとつめのアプローチとして、WorkerそのものがSubjectのインターフェースを備えるというアプローチを試みます。 これはWorkerスレッドから送られてくるイベントをメインスレッドで購読する形です。

f:id:lacolaco:20190326142344p:plain

次のコードは、Workerを隠蔽する WorkerSubject の実装例です。 WorkerSubjectnext メソッドで渡されたデータをWorkerにpostMessageし、 Workerの message / error イベントを内部の子Subjectで購読します。 WorkerSubject を購読する Subscriberは 内部の子Subjectを間接的に購読することになります。 これは実装の一例であって、もっと効率的な実装はあると思います。

export class WorkerSubject<T> extends Subject<T> {
  private inner = new ReplaySubject();
  private sub = new Subscription();

  constructor(public worker: Worker) {
    super();
    this.sub.add(
      fromEvent<MessageEvent>(worker, 'message').subscribe(ev =>
        this.inner.next(ev.data),
      ),
    );
    this.sub.add(
      fromEvent<ErrorEvent>(worker, 'error').subscribe(ev =>
        this.inner.error(ev.error),
      ),
    );
    this._subscribe = this.inner._subscribe.bind(this.inner);
  }

  next(value: T) {
    this.worker.postMessage(value);
  }

  complete() {
    this.sub.unsubscribe();
    this.inner.complete();
    super.complete();
  }
}

具体的な例として、Markdown文字列をHTML文字列に変換する処理をWorkerスレッドで実行してみます。 まずは次のように ./compile-markdown.ts を作成します。

Subjectに隠蔽するためには、入力に対して出力を返すping-pong型のWorkerであると好都合です(必ずしもそうでなくてもよいですが)。 onmessageで受け取った文字列を変換し、 postMessage でレスポンスのイベントを発火しています。

import * as marked from 'marked';

function compileMarkdown(markdownString: string) {
  return new Promise<string>((resolve, reject) => {
    marked(markdownString, {}, (err, result) => {
      if (err) {
        reject(err);
        return;
      }
      return resolve(result);
    });
  });
}

// [tsconfig] lib: "dom" and "webworker" are exclutive.
const _self: Worker = self as any;

_self.onmessage = ev => {
  compileMarkdown(ev.data)
    .then(result => {
      _self.postMessage(result);
    })
    .catch(err => {
      throw err;
    });
};

const _self: Worker = self as any; はTypeScriptのためのハックです。同じtsconfigで domwebworker の両方をターゲットとすることができない問題があるため、手動で self の型をグローバルの Window 型ではなく Worker 型に補正しています。

あとはModule Workerを作って、 WorkerSubject でラップすると使えるようになります。 Angularのコンポーネントで使うと、次のようなコードになります。 結果としてこのコンポーネントのテンプレートには ## foo<h2>foo</h2> に変換されたHTML文字列が表示されます。

@Component({
  selector: 'app-root',
  template: `
    <div>{{ compiled$ | async }}</div>
  `,
})
export class AppComponent implements OnInit {
  compiled$: Subject<string>;

  constructor() {
    // Module Workerの作成とWorkerSubjectでのラップ
    this.compiled$ = new WorkerSubject(
      new Worker('./compile-markdown', { type: 'module' }),
    );
  }

  ngOnInit() {
    // WorkerSubjectに新しいデータを送る
    this.compiled$.next('## foo');
  }
}

このアプローチのメリットは次のものが考えられます。

  • Workerの実装に制約がなく、既存のWorkerはほとんど適用可能である
  • Module Workerがコード分割する境界としてわかりやすく、ES ModuleをそのままWorker化できるのが簡単
  • もともとnext/subscribeでWriteとReadが非同期的であることから、その内部がWorkerを経由していても利用側に影響しない

一方で、Worker側の実装ではpostMessage/onmessageを隠蔽できていないという課題もまだあります。

アプローチ 2. Worker as an Operator

もうひとつのアプローチは、Observableに適用するオペレーターの処理をWorkerスレッドに委譲するものです。 Observableの実体や購読者はメインスレッドにあるまま、データ処理の一部分だけの並行性を高められます。

f:id:lacolaco:20190326150102p:plain

このアプローチの実装はWorkerを関数のように扱うため、Module WorkerよりもgreenletによるインラインWorker化のほうが向いています。 インラインWorkerとは、 Data URIを使って作成されるWorkerのことを指しています。 greenletは、Promiseを返す非同期関数を実行時にインラインWorkerに変換してWorkerスレッドで実行するライブラリです。

github.com

RxJSのオペレーターで、関数を渡して処理をおこなう代表的なものは map 系のものでしょう。 どのオペレーターにも適用できますが、ここでは map オペレーターをWorker化した mapOnWorker オペレーターを実装してみます。

RxJSのオペレーターの実体はObservableを受け取ってObservableを返す関数です。 mapOnWorker は次のように簡単に実装できます。

import gleenlet from 'greenlet';
import { from, Observable } from 'rxjs';
import { concatMap } from 'rxjs/operators';

export function mapOnWorker<T, U>(fn: (arg: T) => Promise<U>) {
  // 関数をインラインWorker化する
  const workerized = gleenlet(fn);
  return (source: Observable<T>): Observable<U> => {
    // 1. `workerized`関数を呼び出す
    // 2. 戻り値のPromiseを `from` 関数でObservableに変換する
    // 3. `concatMap` オペレーターで元のObservableと結合する
    return source.pipe(concatMap(v => from(workerized(v))));
  };
}

map オペレーターと同じように順序を守るために concatMap を使いましたが、mergeMapswitchMap のようなオペレーターを使うものも簡単に作れます。

export const mapOnWorker = concatMapOnWorker;

export function concatMapOnWorker<T, U>(fn: (arg: T) => Promise<U>) {
  const workerized = gleenlet(fn);
  return (source: Observable<T>): Observable<U> => {
    return source.pipe(concatMap(v => from(workerized(v))));
  };
}

export function switchMapOnWorker<T, U>(fn: (arg: T) => Promise<U>) {
  const workerized = gleenlet(fn);
  return (source: Observable<T>): Observable<U> => {
    return source.pipe(switchMap(v => from(workerized(v))));
  };
}

export function exhaustMapOnWorker<T, U>(fn: (arg: T) => Promise<U>) {
  const workerized = gleenlet(fn);
  return (source: Observable<T>): Observable<U> => {
    return source.pipe(exhaustMap(v => from(workerized(v))));
  };
}

Workerへの関心はオペレーターの内部に完全に閉じているので、オペレーターの利用側は他のオペレーターと同じようにただ pipe メソッドに渡すだけです。

import { interval, Observable } from 'rxjs';
import { mapOnWorker } from '../lib/mapOnWorker';

@Component({
  selector: 'app-root',
  template: `
    <div>{{ calculated$ | async }}</div>
  `,
})
export class AppComponent implements OnInit {
  calculated$: Observable<any>;

  constructor() {
    // 1msごとに発火するObservable
    this.calculated$ = interval(1).pipe(
      // Workerで計算処理を実行する
      mapOnWorker(async i => Math.sqrt(i)),
    );
  }
}

このアプローチのメリットは、オペレーター利用側にまったく関心を漏らさずにCPU負荷の大きいオペレーター処理をWorkerスレッドに逃がせるところです。 上記の例では非同期化するまでもない処理ですが、文字列の全文検索だったりパターンマッチだったり、メインスレッドをブロックしうる計算処理がObservableのオペレーターにあるときには有効です。

デメリットはオペレーターの呼び出しのたびにかかるインラインWorkerとのコミュニケーションのコストです。 Workerスレッドで実行する処理があまり時間のかからないものであれば、オーバーヘッドが相対的に高く付くこともあるかもしれません。

まとめ

この記事ではWeb Workerを意識せずにWeb Workerの恩恵を受けられるようにRxJSを使って抽象化するアプローチを紹介しました。 Promiseを使ってクラスや関数をWorker化するアプローチは Google Chromeチームの Comlink や Cloonyがとてもクールです。 しかし複数回発行するイベントを扱うにはどうしてもObservableのようなモデルが必要だと思います。

github.com

github.com

サンプルコードはGitHub上で公開しています。 コード例はどれも完璧である保証はなく、もっと効率的な実装があるかもしれませんので、ご利用は自由ですが自己責任でよろしくおねがいします。

github.com

読後メモ: 「MaaS モビリティ革命の先にある全産業のゲームチェンジ」

いつもの読後メモ。 今回は日高 洋祐他著の「MaaS モビリティ革命の先にある全産業のゲームチェンジ」。

MaaS モビリティ革命の先にある全産業のゲームチェンジ

MaaS モビリティ革命の先にある全産業のゲームチェンジ

本の概要

この本はMaaS(Mobility as a Service)について、基本的な考え方から実践的な事例、今後を見据えたアクションプランまで網羅的に解説している。 海外の事例は実際に体験した生の情報が多く書かれていて、現実感と納得感のある内容だった。

本書には専門的な内容も含まれるが、おのおの興味のある章から読んでいただき、そこから関連する章に興味の赴くまま読み進んでもらえれば幸いだ。読者の皆様が、広く深く「MaaSの世界」に入り、ビジネスを成功させるうえで役立つものとなることを願っている

.

そこで、海外を含めて少しでも筆者たちの見聞きしたこと、感じたこと、考えたことを日本に伝えたい。また、MaaSの「本質」及びその「先」にある交通および社会、あらゆる産業のビジネスモデルの変革が、果たして危機なのか、輝ける未来なのか。モビリティの世界に閉じるのではなく、日本再興を期する全産業のチャンスとして捉え、MaaSのその先にある「Beyond MaaS(ビヨンド・マース)」の答えを、本書をきっかけとして読者の皆様と創り上げていきたい。これこそが、筆者たちが本書を世に問う一番の動機である。

かなり濃厚でMaaSについて全体像から細部まで解説されていて、読み応えがあった。MaaSについて知りたい人はぜひ一冊目に読むと良い本だと思う。

内容の紹介

本の序盤ではMaaSとはいったい何を指し、何を指さないのかについて、観念的な話題を中心にしている。

日本ではウーバーテクノロジーズに代表される配車サービスなどの単一のモビリティサービスを指してMaaSと呼ぶ向きもあるが、それはMaaSを構成する一要素でしかない。利用者視点に立って複数の交通サービスを組み合わせ、それらがスマホアプリ1つでルート検索から予約、決済まで完了し、シームレスな移動体験を実現する取り組みが、グローバルスタンダードで示すところのMaaSである。

MaaSは「理想の移動体験」の実現を目的としたひとつの手段であることが肝要である。 その発祥は北欧、フィンランドにある。およそ30年ほど前から、ヨーロッパでは自動車社会からの脱却を目指すムーブメントが起こっている。

自動車利用に依存した社会からの脱却の1つとしてフィンランドから生まれた新たなサービスがMaaS(Mobility as a Service、マース)であり、世界中で注目されるようになった。  MaaSとは、従来のマイカーや自転車などの交通手段をモノで提供するのではなく、サービスとして提供する概念である

30年、ほとんど平成まるごと出遅れているわけだが、日本でも2018年にようやく政府の戦略の中にはっきりと「MaaS」が提言されている。日本のモビリティ革命はこれから始まる。

一方、日本では、政府の成長戦略として 18 年6月に閣議決定された「未来投資戦略2018」において、初めて「Society 5・0」の実現のためのフラッグシッププロジェクトとして、MaaSが位置付けられた

中盤からは、具体的なMaaSの事例をもとに、どのように社会実装されるのが望ましいのかについて考察している。

キーとなるのは、マイカー依存を脱却することで浮く財源を公共交通機関へ流していくことである。

公共交通の質を高めるためには投資が必要だが、マイカー依存が進んだ社会で、公共交通に投資をするのは難しい。利用者が少ないから利益が出ないし、だからと言って税金を投入しようにも、マイカー利用者からの支持を得ることが難しいから

公共交通への投資が進めばよりマイカー無しで暮らしやすい地域ができていく。MaaSはその好循環を生み出す鍵になる。

つまり、MaaSがビジネスとして成功するほどに、地域でマイカーのエコシステムを維持するために使われていた資金が、公共交通を潤すことになるのだ。  公共交通に資金が回るようになれば、必要な投資ができるようになり、公共交通の質が改善する。それは公共交通の利便性・快適性を高めるから、公共交通へのシフトがより進む。こうした好循環によって公共交通の質が改善していくことが期待される。

日本の地方都市は車への依存度が高すぎて、駅前は自動車用の広大なロータリー、市街地の一等地がどんどん駐車場に変わっている。誰も歩かないから店が寂れ、駐車場に変わり、さらに歩いて訪れる場所が減っていく悪循環に入っている。

完全にクルマ社会になっている日本の地方都市は、一般に、歩いていける範囲に出ていきたくなるような場所がない。中心市街地は寂れているから、休日の過ごし方といえば、特定の趣味がある人を除き、郊外のショッピングセンターに行くのが関の山ということになる

ヨーロッパでは10年以上先行した脱マイカー依存の取り組みにより、歩いて楽しい街づくりが出来上がっている。 これからの地方都市は歩いて楽しいコンパクトな町をどうやって作るかが重要になる。

対する欧州の地方都市は、そんなに大きくなくても中心市街地に常に人の往来があり、にぎわいがある。中心部には路面電車が走り、クルマがなくとも移動ができて、ウィンドーショッピングをしたり、公園やカフェでのんびりしたりできる。休日は広場にファーマーズマーケットが立つから、朝から大勢の人でごった返す。すべての地方都市がそうだというわけではないが、衰退していない欧州の地方都市に共通するのは、歩いて楽しい町、クルマがなくても移動に困らない町になっているということである。

.

歩いて楽しくて、移動に困らない町になっているのは、そういう方向での足づくりとまちづくりの努力を弛まずに続けてきたからだ。クルマ社会になるに任せて無計画にまちづくりをしてきた日本とはそこが大きく異なっている。

終盤からは、MaaSが与えるこれまでの産業、経済構造への影響や、これから各事業者がどのようなアクションを取っていくべきかの提言になっている。 キーとなるのは新たなモビリティを社会に投入していくための規制緩和と、それらが統合されるために官民で取り組むプラットフォームづくりである。

それぞれ独立して企業活動をしてきた鉄道会社や自動車メーカーも、新たに生まれるMaaSの統合プラットフォームの下でのビジネスになりかねない。音楽業界やエンタメ業界、旅行業界、出版業界など、さまざまなコンテンツ業界がプラットフォームビジネスの波に飲み込まれていることと同じだ。 「自分たちの仕事だけ頑張っていればいい」という時代は終わった。既存事業の延長線上でMaaSを捉えないほうがいい。世界がそう変わっていく、実際に変わりつつあることは、MaaSに取り組む理由を考えることと並行して念頭におくべきであろう


他にも紹介したい引用はいっぱいあるが続きは読んでほしい

良い設計と平衡

免責事項: 思考過程のメモです

アプリケーションの設計とは

アプリケーションの設計とは、「空間を定義し」「問題を識別し」「解決手段を選択する」工程である。

空間の定義

システムの中で、設計の対象とする領域とそうでない領域を定義する。 システムへの要求により空間は大きくなる。フレームワークは空間を狭める効果がある。 システムの空間のなかで設計の対象範囲を広く定義するほど「設計の自由度が高い」とみなせる。 設計の自由度が高ければ選択できる解決手段も増えるが、同時に問題の量も増える。

問題と解決

設計空間の中で、問題を識別し、それを解決できる手段を選択する。

f:id:lacolaco:20190317214301p:plain

システムの問題を解決するためにはシステムに作用する必要があり、その作用により別の問題が発生することが常である。 トレードオフと呼ばれ、解決前の問題と解決後の問題の重みを比較し、どちらを受け入れるかを選択する。 結果として、問題と解決、それによる問題を補う解決、というようにネットワークがつながっていく。

f:id:lacolaco:20190317214659p:plain

問題を解決するための手段はほとんどの場合複数ある。 複数の問題を一挙に解決できる手段もある。 そして目先の問題を解決するための手段が連鎖の先で大きな問題を誘起することもある。 良い設計は、全体としてネットワークの分岐やサイズをコンパクトに抑える。

f:id:lacolaco:20190317214956p:plain

すべての問題が解決されることはない。ネットワークの末端や途中には未解決の問題が残る。

f:id:lacolaco:20190317215641p:plain

それらを解決することでさらにシステムを拡張する選択もあるが、解決しないという選択を取ることもできる。 それは仕様と呼ばれたりレガシーと呼ばれたり負債と呼ばれたり、認識はいろいろである。 その問題から先のチェーンをふるい落とし、システムをコンパクトに維持するための先送りである。

f:id:lacolaco:20190317215812p:plain

そのようにして設計されたシステムは、全体として釣り合いがとれた平衡状態になる。

f:id:lacolaco:20190317215933p:plain

システムは時間とともに外部からの刺激を受ける。 新しい機能の追加、仕様の変更、あるいはインフラ環境の変化など。 それらはシステムの中で新たな問題を生み出す。

f:id:lacolaco:20190317220409p:plain

新たな問題を解決するためにまたシステムに手を加える必要が生まれる。 良くない設計は、新たな問題に対して必要な変更が大きいシステムを生み出す。柔軟性が低く、衝撃を吸収できない。メンテナンス性が低いともいう。 新たな作用により既存の問題と解決に影響してしまうこともあり、ネットワーク全体で新しい平衡に達するまでに時間がかかる。

f:id:lacolaco:20190317220637p:plain

良い設計は新しい平衡に達するまでの時間が短い。 あらかじめ新たな問題の発生を予測してあるシステムは、解決のために既存のネットワークに加える変更が小さい。

f:id:lacolaco:20190317220918p:plain

どう設計すべきか

現在観測できる問題をスマートに解決できるシステムであっても、未来に受ける新たな外部刺激から生まれる問題に弱くては良い設計とは言えない。 かといって最初からあらゆる問題を想定することは早すぎる最適化のような別の問題も引き起こし、開発コストも増加する。 どこまでを現在の設計でカバーし、どこから先を未来の再設計に先送りするかの選択こそが必要である。それすらもトレードオフである。

良い設計を生み出すために磨くべき能力は

  • プロジェクト、チームメンバーなど外部要因に合わせた設計空間定義
  • 問題を見逃さない目。多くの問題は一般的に発生するパターンがある。
  • 解決手段の引き出し。より多くの選択肢の中から最良の決定をする。
  • 想像力。一度達した平衡が崩れる将来リスクを認識し、備える。

Angularで巨大なライブラリを動的に読み込む

オリジナルはこちら

medium.com

基本的にコードサンプルなどはオリジナルを参照してください。この記事では込み入った事情の部分だけを日本語で補足します。

tsconfig.jsonの準備

tsconfig.jsonmodule 設定は、TypeScript内で記述したモジュールのimport/exportをどのように解決するかを指定します。 Angular CLIのデフォルトでは module: es2015 を指定しているので、静的な import ... from はそのまま残しますが、import() はサポートしていません。 tsconfig.jsonmodule: esnext を指定すると、import()JavaScriptにそのまま残すようになります。 import() がサポートされたブラウザ上であれば、webpackを通さなくてもそのままブラウザ上でモジュール解決できる状態になっています。

ところがまだ import() はTC39のProposalとしてはStage 3で、未サポートのブラウザが多くあります。 現実的には、webpackを使ってbundleする必要がありますが、webpackはこの import() をwebpackがもつ動的モジュール読み込みの仕組み ( require.ensure ) で置き換えてくれます。 つまり、 import() のpolyfillのように振る舞ってくれます。

webpackを通すことでbundle後のJavaScriptには import ... fromimport() も残らないため、 target: es5 のままトランスパイルしても問題ありません。つまりブラウザ互換性には影響しません。 Promiseがないブラウザではes2015のpolyfillが必要ですが、Angular CLI v7.2からはデフォルトで es2015非サポートなブラウザでだけ自動的に適用されるpolyfillを吐き出すので、我々がes2015のpolyfillについて気にすることはありません。

normalizeCommonJSImport について

これは TypeScriptの import() の型定義でCommonJSとの互換性に問題があるための処置です。 import() では名前付きインポートをサポートしておらず、ES Moduleにおける default export だけをサポートしています。 webpackではその互換性のために、CommonJSで書かれたモジュールを import() でインポートするときには、module.exports オブジェクトを default exportに見立てて、 import() で読み込まれるオブジェクトの default プロパティに格納しています。

TypeScriptの import() は賢いので、 静的にimportしたときに import * as Chart from 'chart.js' で得られる Chart の型と、 import('chart.js').then(result => result) で得られる result の型は同じに扱うのですが、実際は result.defaultChart に相当するので、素直に書くとTypeScriptのコンパイルが通りません。 そのために normalizeCommonJSImport でラップしています。

転職のお知らせ

f:id:lacolaco:20190227232704j:plain

写真は妻が作った雪だるまです。

  • From: 株式会社Kaizen Platform
    • 2/28が最終出社日でした
    • やってたこと
      • Webフロントエンド SPA開発 (React/TypeScript)
      • Schema-first GraphQLによるAPI仕様中心開発の整備
      • UX/UI設計
      • Webパフォーマンス計測、改善
      • その他
  • To: bitbank株式会社
    • 4月から入社します
    • 週4日にしてもらいました
    • やりたいこと
      • ある程度の規模に育ったAngularアプリの開発に関わりたい
      • Angularのエキスパートとして持てる力を尽くしてチームを加速させたい
      • Web技術とブロックチェーンのこれからについて、持論を持てる程度の学びを得たい
        • ついでにマイクロペイメントまで学びを得られたら嬉しい

本件についてのお問い合わせは Kyashの送金メッセージでお願いします。(返答を保証するものではありません)

f:id:lacolaco:20190227235324p:plain