余白

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

読後メモ「THE CULTURE CODE 最強チームをつくる方法」

いつもの読後メモです。

今回読んだのはダニエル・コイル著の「THE CULTURE CODE」の日本語訳版。

THE CULTURE CODE 最強チームをつくる方法

THE CULTURE CODE 最強チームをつくる方法

この本は「チームの文化」というそこにあるのは確かなのに実体がつかめないダークマターのようなものに焦点を当てる。

実在の成功しているチーム、失敗しているチームへのインタビューや調査、そして多くの心理学的、社会学的実験のデータから、成功するチームのつくり方を明らかにしていく。全体を通して非常にエビデンス重視になっている。

感想

ここ最近で読んだ本の中で一番面白かった。

これからチームを作る人、チームを任された人、あるいは自分のチームが十分に成功していないと感じる人には、この本をぜひ読んでもらいたい。 チームを成功させるためにやるべきこと、やってはいけないこと、その基本がこの本には多くのエピソードやエビデンスと共に語られている。 エピソードが多いため登場する人名も多いが、全体を通して読みやすく翻訳されている。

よくGoogleNetflixのような一流企業のチームは素晴らしい文化を持っていると語られる。 この本を読むと、そうした文化は先天性のDNAのようなものではなく、スキルによって勝ち取り、育み、支えられているものだとわかる。 それは優れたリーダーが扇動するだけで出来上がるものでもない。日々の積み重ねで価値観を醸成していかなければならない。

この本では「心理的安全性」という言葉は使われないが、この本の中で「成功しているチーム」として挙げられるチームは例外なく心理的安全性が高い。 心理的安全性を高める方法に悩んでいる人にとっても、良いガイドになるだろう。

成功するチームには文化がある。その文化を作るスキルの磨き方を教えてくれる本だった。

印象深かったフレーズ

強いチームの文化を醸成するためのカギは、高度なスキルを持つ優秀なメンバーを集めることにはない。迅速な意思決定と実行でもない。そもそも「強いリーダー」は必要ない。個性的でエキセントリックな天才も必要ない。野心的で挑戦的なビジョンは不要。最先端のテクノロジーも無用。決め手は日常の仕事での、ちょっとしたさりげない行動──それはしばしば当人も意識していない──にある。小さな行動の積み重ねが大きな違いを生み出す。  強いチームのエンジンに火をつけるのはいたって常識的な「普通の人」である。

多くのエピソードが紹介されるが、キーマンとなるのは特別な才能を持ったリーダーではなく、人当たりのよい常識人であることが多い。そういう人柄こそがチームをうまく機能させる。

チームの文化が大切だということは誰でも知っている。しかし、そのしくみとなると、きちんと理解している人はほとんどいない。... しくみがよくわからないのは、もしかしたら「文化」というものを誤解しているからかもしれない。私たちは、文化はDNAのようなものだと考えている。  強固なチームの文化というと、たとえばグーグルやディズニー、ネイビーシールズなどが思い浮かぶだろう。彼らには固有の文化がある。あまりに独自の文化なので、彼らにしかないDNAから生まれたとしか思えない。  つまり、文化は運命のようなものであり、努力でどうにかなるものではない。強固な文化を持つチームもあれば、持たないチームもある。運命とはそういうものだ。  しかし、この本はその考え方に賛同しない。私はこの4年の間に、世界でもっとも成功している8つのチームを実際に訪ね、分析を重ねてきた。たとえば、軍の特殊部隊、都市部の貧困地区にある公立学校、プロのバスケットボールチーム、映画スタジオ、コメディ集団、宝石窃盗団などだ※2。  分析の結果、それらのチームには共通のスキルがあることがわかった。

文化とはスキルによって作られるものである。というこの本の主題。

たいていの人は、「言葉は言葉でしかない」とは考えない。むしろ大切なのは言葉であり、チームのパフォーマンスはメンバーの「言葉の知性」と比例し、複雑なアイデアを的確に伝える能力が高いほど、チームのパフォーマンスも上がると考えている。  しかし、その考え方は間違っている。言葉はノイズだ。  チームのパフォーマンスを決めるのは、「ここは安全な場所だ。そして私たちはつながっている」というメッセージを伝えるしぐさや態度なのだ。

多くの場合、人を動かすのは言葉ではなく仲間の振る舞いだ。という集団的知性の話。

人間の脳が「安心」を合理的に理解するのであれば、シグナルは一度で十分なはずだ。しかし私たちの脳はそのように進化していない。脳のいちばんの仕事はむしろ心配することであり、とりつかれたように「危険」のシグナルを探している。だからこそ人類は生き残ることができた。  この危険を執拗に恐れる態度は、脳の奥深くにある扁桃体という部位から生まれている。扁桃体は原始的な脳の部位で、つねに周りの環境を監視して危険を探している。

繰り返し帰属のシグナルを送り続けなければならない理由について。「ここにいても安全か」という心配は本能であり、どこかに危険なサインがないかをいつも探している。

帰属のシグナルに、人格や規律は関係ない。大切なのは、メンバーが安心できる環境をつくることだ。 「私たちはつながっているか?」 「私たちは未来を共有しているか?」 「ここは安全な場所か?」  という問いに対して、「イエス」という答えを与えられる環境がカギになる。

「メンバーが安心できる環境」とはつまり心理的安全性の高いチームであるということだ。その環境をどう作るかという話が細かく書かれている。

いいチームをつくるうえでいちばん大切なのは、優秀なメンバーを集めることでも、経験豊かなメンバーを集めることでもない。それは、メンバーの机の位置だ。 「目で見えるといったごく単純なことが、とても大きな意味を持つ」とアレンは言う。「他のメンバーの姿が見える、他のメンバーが働いている場所が見えるといったことが、彼らの存在を思い出させるきっかけになってくれる。それが大きな力を持つようになる。

すべては 「私たちはつながっているか?」 「私たちは未来を共有しているか?」 「ここは安全な場所か?」 を確認できるということに通じる。

成功しているチームは、メンバー選びの段階からすでに成功している。 ... 成功しているチームは、腐ったリンゴに対してとても厳しい。それに加えて、腐ったリンゴを鋭く見抜くこともできる。おそらく後者のほうが、より重要な資質だろう。 「オールブラックス」の愛称で知られるラグビーニュージーランド代表チームは、世界史上もっとも成功したスポーツチームの1つに数えられる。そんな彼らのモットーは、「愚か者は去れ」だ。シンプルな言葉だが、だからこそ大きな効果がある。

この本を読むとわかるが、チームに悪影響を与えるメンバーというのは明確に存在する。非協力的なメンバーを「腐ったリンゴ」と表現しているが、そのようなメンバーがひとりいるだけで他のメンバーによってそこが危険な環境になってしまうのだ。なので、チームメンバーは慎重に選ばなければならないし、チームのためにならないメンバーは改心してもらうか、出ていってもらうしかない。

成功しているチームは、チームワークが生まれるのを偶然にまかせたりはしない。メンバーに期待されていることを明確にしている。そして言葉や態度で、協力することの大切さを何度も伝える。

いいチームワークを生むためにメンバーにはメンバーの責任がある。リーダーがそれを主導する。

成功しているチームは、実行しやすいように、ごくシンプルなしくみをつくっている。たとえば、「すべてのミーティングでかならず全員が発言する」というようなルールである。

チームワークを高められるように仕組みとして当たり前化する。エピソードとして登場する多くのチームが何らかの習慣を持っている。

次に読む本

OKRについての本を読むつもり。

OKR(オーケーアール) シリコンバレー式で大胆な目標を達成する方法

OKR(オーケーアール) シリコンバレー式で大胆な目標を達成する方法

読後メモ: 「エンジニアリング組織論への招待」

いつもの読後メモ。

今回は巷で大絶賛の「エンジニアリング組織論への招待」を読んだ。

感想

いままで曖昧に理解していたものの、いざ自分で話そうとすると言語化できなかっただろうたくさんの概念を、改めて理解できた部分が大きい。

「エンジニアリング」とは何か。「エンジニアリング組織」とは何か。そういった根底から改めて考え直すいい機会になった。

中でも「不確実性」という概念はさまざまな不安、問題と向き合うときの心強い味方になりそうだと思った。

印象深かったフレーズ

人間にとって、本質的に「わからないこと」はたった2つしかありません。それは、「未来」と「他人」です。

未来がわからないことによる「方法不確実性」と「目的不確実性」、そして他人の考えがわからないことによる「通信不確実性」の3種類をどう扱っていくか、という話。

仮説思考は、経験主義をさらに生産的な(不確実性を削減する)ものにするための「大胆な跳躍」をもたらします。そして、仮説は、今あるデータからは、演繹的・帰納的には導くことのできないものです。人間的な直感やひらめきによって、今までの情報や様々な偶然が積み重なって生まれる跳躍であって、天下り的な結論や合議による凡庸なアイデアは「仮説」にはなりえないのです。

データや前提から導けないからこそ仮説検証の意味がある、という「言われてみればたしかにそうだ」と納得したフレーズ。

メンタリングでは、見えていない課題に自分から気づかせることを重視します。自分で気がついたことのほうが、積極的に解決することができるからです。

そうだよな〜〜、うまくそうやって導けるようになりたいと思う。

メンタリングでは、「次にとるべき行動」がはっきりするように促す必要があります。それが曖昧なままでは「悩み」は継続します。しかし、「次にとるべき行動」がはっきりすれば、「考える」ことはあっても、「悩むこと」は少なくなるでしょう。

「悩む」と「考える」の違いについて。悩んでいる人を、考えるフェーズに送り出すのがメンターの役割なんだなあと学びになった。

従業員にとって明示的でない権限は、最も不自由な状態とちがいがありません。権限が明示的でないことが意味しているのは、上司の胸先三寸で権限について差配できるということです。これは実質、すべての権限が上司にある状態と変わらないのです。

責任が曖昧な権限は「タダより高いものはない」と同じような状態ってことだと思う。セットで与えられるからこそ自律できるしモチベーションも出てくる。

アーキテクチャとは、システムのどのポイントが「変更しやすく」どのポイントが「変更しにくい」のかを見極めて、構造として組み込むものです。 そのため、負のアーキテクチャである「技術的負債」は、変更していくだろうと思っていたポイントがあまり変更しなかったときと、変更しないだろうと思っているポイントが変更されるときに生まれます。

技術的負債が「負のアーキテクチャ」であるという捉え方は目からウロコだった。いい語彙をもらえた。

次に読む本

次は、Kindleストアにおすすめされた「THE CULTURE CODE」を読む。 少し読み始めたところだが、「エンジニアリング組織論への招待」を含め、昨今広く語られるようになった「心理的安全性」というものの源泉について解き明かせるような気がする。 安心できるチームとそうでないチームの差、「帰属のシグナル」、またいい語彙がもらえそうな本なのでわくわくしている。

THE CULTURE CODE 最強チームをつくる方法

THE CULTURE CODE 最強チームをつくる方法

NgRx v7.4で導入されるAction Creatorの使い方

この記事では NgRx v7.4で導入される Action Creator 機能と、それを使った実装パターンを紹介します。 Action Creatorはまだ ngrx.ioドキュメンテーションに含まれていませんが、将来的に追加された後はそちらを参照するようにしてください。

アクションの定義

簡単なカウンターを実装しながら、これまでのNgRxの書き方をおさらいしましょう。 今回のカウンターは、任意の数値を受け取って加算する Increment と、カウンターをリセットする Reset をアクションとして定義します。

これまでのアクション定義では、アクションタイプのEnum と、それを持つ各アクションクラス、そしてそのクラス型のUnion Typeを定義するのが一般的でした。 たとえば IncrementReset というアクションとする counter.actions.ts を定義すると次のようになります。 Increment は与えられた数だけカウントを進め、 Reset は カウントを 0 に戻すためのアクションです。

// counter.actions.ts
import { Action } from '@ngrx/store';

export enum ActionTypes {
  Increment = '[Counter] Increment',
  Reset = '[Counter] Reset',
}

export class Increment implements Action {
  readonly type = ActionTypes.Increment;

  constructor(public payload: number) { }
}

export class Reset implements Action {
  readonly type = ActionTypes.Reset;
}

export type ActionsUnion = Increment | Reset;

このファイルはAction Creatorによって次のように書き換えられます。

// counter.actions.ts
import { createAction, union } from '@ngrx/store';

export const increment = createAction(
  '[Counter] Increment',
  (payload: number) => ({ payload })
);

export const reset = createAction(
  '[Counter] Reset'
);

const actions = union({
  increment,
  reset,
});

export type ActionsUnion = typeof actions;

createAction 関数

まずクラス定義を置き換えている createAction 関数について解説します。 この関数は Action Creatorを返します。Action Creatorはアクションオブジェクトを返す関数です。 つまり、ディスパッチするアクションが、クラスをnewしたインスタンスから関数の戻り値に変わります。

import * as Actions from './actions';

// アクションクラスのインスタンス
store.dispatch(new Actions.Increment(1));

// Action Creator
// 関数がActionを返す
store.dispatch(Actions.increment(1));

引数を取るアクションは、 createAction 関数の第2引数に関数を渡します。 この関数は任意の引数を取り、任意のオブジェクトを返します。 これは従来のアクションクラスにおけるコンストラクタとクラスフィールドの定義と同じです。

increment アクションをもう一度見てみましょう。 第2引数は数値を payload 引数として受け取る関数で、戻り値は payload プロパティをもつオブジェクトです。。 この関数の戻り値は第1引数から作られるアクションオブジェクトとマージされ、 最終的に { type: '[Counter] Increment'', payload } というアクションオブジェクトを作成することになります。

// アクションを作成する
const action = Actions.increment(1);

// アクションオブジェクトは `type` を持つ
console.log(action.type); // => '[Counter] Increment'
// 第2引数で返したオブジェクトがマージされている
console.log(action.payload); // => 1

ちなみに、これまで Enumで管理していたアクションタイプの文字列は、これまではクラスインスタンスを作らないと type が手に入らないためにクラスと別にEnumを置いていましたが、 今後は increment.type という形でアクセスできるため、いちいちEnumを作る必要はありません。 これについては後述する Reducerの変更部分で詳しくわかります。

union 関数

一連のアクションの型を合成したActionsUnion 型は、ReducerやEffectなどいくつかの場所で必要となります。 従来のアクションクラスでは、クラス型の Union Type をそのまま扱えたが、関数の場合はその関数の戻り値の型を合成する必要があります。 それを補助してくれるのが NgRxの union 関数です。

すべてのAction Creatorunion 関数に渡し、その戻り値を エクスポートせず 宣言します。 なぜエクスポートしないかというと、欲しいのはその型だけだからでです。エクスポートして外部から参照可能にしたところで使いみちはありません。 actions 変数を宣言したら、typeof を使ってその型を Union 型として外部にエクスポートします。

// 戻り値はエクスポートしない
const actions = union({
  increment,
  reset,
});

// 型だけエクスポートする
export type ActionsUnion = typeof actions;

Reducerの作成

Action Creatorを定義したら、次はReducerを対応させます。 もともとアクションクラスとEnumを使っていたときは、次のような Reducerになっていました。 引数に渡されるアクションの型は ActionsUnion 型で、 action.typeActionTypesEnum文字列と照らし合わせるswitch文を記述します。

import { ActionsUnion, ActionTypes } from './actions';
import { State, initialState } from './state';

export function reducer(state = initialState, action: ActionsUnion): State {
  switch (action.type) {
    case ActionTypes.Increment: {
      return {
        ...state,
        count: state.count + action.payload,
      };
    }
    case ActionTypes.Reset: {
      return {
        ...state,
        count: 0,
      };
    }
    default: {
      return state;
    }
  }
}

このReducerに先ほどの アクション定義の変更を反映すると、次のようになります。 変わったのはcase文だけです。 case文で指定するアクションタイプは、Action Creatorがもつ type プロパティに変わりました。 このように Action Creatorから直接取得できるため、アクション定義側でEnumに分離する必要がなくなっています。

import { ActionsUnion, increment, reset} from './actions';
import { State, initialState } from './state';

export function reducer(state = initialState, action: ActionsUnion): State {
  switch (action.type) {
    case increment.type: {
      return {
        ...state,
        count: state.count + action.payload,
      };
    }
    case reset.type: {
      return {
        ...state,
        count: 0,
      };
    }
    default: {
      return state;
    }
  }
}

Effectsの作成

NgRxのEffectsを使って、カウンターの加算とリセットがおこなわれるたびにログを出力する副作用を定義します。 従来のアクション定義では次のようになります。

import { Injectable } from '@angular/core';
import { Effect, Actions, ofType } from '@ngrx/effects';
import { tap } from 'rxjs/operators';

import { ActionsUnion, ActionTypes } from './actions';

@Injectable()
export class CounterEffects {

  constructor(private actions$: Actions<ActionsUnion>) { }

  @Effect({ dispatch: false })
  logger$ = this.actions$.pipe(
    ofType(ActionTypes.Increment, ActionTypes.Reset),
    tap(action => {
      console.log(action);
    }),
  )
}

これも Reducerと同じように、アクションタイプの部分だけに影響があります。

import { Injectable } from '@angular/core';
import { Effect, Actions, ofType } from '@ngrx/effects';
import { tap } from 'rxjs/operators';

import { ActionsUnion, increment, reset } from './actions';

@Injectable()
export class CounterEffects {

  constructor(private actions$: Actions<ActionsUnion>) { }

  @Effect({ dispatch: false })
  logger$ = this.actions$.pipe(
    ofType(increment.type, reset.type),
    tap(action => {
      console.log(action);
    }),
  )
}

アクションのディスパッチ

最後にアクションをディスパッチする部分です。 従来のアクションクラスでは、クラスインスタンスを生成して次のようにディスパッチしていました。

import * as CounterActions from './state/counter/actions';

@Component({
  selector: 'my-app',
  template: `
     <div>{{ count$ | async }}</div>
     <button (click)="incrementOne()">+1</button>
     <button (click)="reset()">Reset</button>
  `,
})
export class AppComponent {

  count$ = this.store.pipe(
    select(state => state.counter.count),
  );

  constructor(private store: Store<AppState>) { }

  incrementOne() {
    this.store.dispatch(new CounterActions.Increment(1));
  }

  reset() {
    this.store.dispatch(new CounterActions.Reset());
  }
}

これはすでに説明したとおり、Action Creatorの関数を呼び出した戻り値をディスパッチするように変わります。

import * as CounterActions from './state/counter/actions';

@Component({
  selector: 'my-app',
  template: `
     <div>{{ count$ | async }}</div>
     <button (click)="incrementOne()">+1</button>
     <button (click)="reset()">Reset</button>
  `,
})
export class AppComponent {

  count$ = this.store.pipe(
    select(state => state.counter.count),
  );

  constructor(private store: Store<AppState>) { }

  incrementOne() {
    this.store.dispatch(CounterActions.increment(1));
  }

  reset() {
    this.store.dispatch(CounterActions.reset());
  }
}

これですべての置き換えが終わりました。

Action Creatorのメリット

クラスで定義されるアクションは、インスタンスを作るまで type にアクセスできない不便さや、形式的に書かなければならないコードの量が多かったのが課題でした。

Action Creatorでは関数で記述できるので、無駄なコードが大きく減ります。 そして機能やテスタビリティは以前と変わらず、特にデメリットはありません。

プロジェクトのNgRxをv7.4にアップデートしたら、基本的にはAction Creatorへの置き換えを進めるべきです。

まとめ

  • アクションをクラスではなく関数で定義する Action Creator を作る createAction 関数が導入された
  • ActionTypeのEnumはもう必要ない
  • ReducerやEffects、ディスパッチ側への影響はとても軽微

この記事で扱ったカウンターアプリケーションが実際に動作する様子を確認してみてください。

stackblitz.com

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