Skip to content

第8章: タッパーウェア

強力コンテナ

http://blog.dwinegar.com/2011/06/another-jar.html

これまで純粋関数を通じてデータを流すプログラムの書き方を学びました。これらは振る舞いの宣言的仕様です。しかし、制御フローやエラーハンドリング、非同期アクション、状態管理、そしてあえて言うなら副作用はどうでしょうか? 本章では、これら有用な抽象化の基盤を発見していきます。

まずコンテナを作成します。このコンテナはあらゆる型の値を保持できなければなりません。タピオカプリン専用のチャック付き袋など役に立ちません。オブジェクトとして実装しますが、オブジェクト指向的なプロパティやメソッドは与えません。宝物箱のように扱います──貴重なデータを優しく包み込む特別な箱です。

js
class Container {
  constructor(x) {
    this.$value = x;
  }
  
  static of(x) {
    return new Container(x);
  }
}

最初のコンテナです。思慮を込めてContainerと命名しました。newキーワードを頻繁に書かず済むよう、Container.ofをコンストラクタとして使用します。of関数には奥深い意味がありますが、現時点ではコンテナに値を安全に格納する適切な方法と考えてください。

新しく作った箱を調べてみましょう...

js
Container.of(3);
// Container(3)

Container.of('hotdogs');
// Container("hotdogs")

Container.of(Container.of({ name: 'yoda' }));
// Container(Container({ name: 'yoda' }))

Node.js環境ではContainer(x){$value: x}と表示されます。Chromeは型を正しく出力しますが、本質的には問題ありません。Containerの構造が理解できれば充分です。環境によってはinspectメソッドを上書きできますが、ここでは詳細に触れません。教育的・美的観点から、概念的な出力をinspectを上書きしたかのように記述します。

次に進む前に明確にしておきます:

  • Containerは1つのプロパティを持つオブジェクトです。多くのコンテナは単一の値を持ちますが、複数も可能です。プロパティ名は任意に$valueと命名しました。

  • $valueの型を特定の型に固定してはいけません。さもなくばContainerの名前に反します。

  • Containerに入ったデータは内部に保持されます。.$valueで取り出せますが、それは本来の目的を損ないます。

この設計の理由は後ほど明らかになりますが、今はしばらくお付き合いください。

ファンクター入門

コンテナ内の値(どんな型でも)に関数を適用する方法が必要です。

js
// (a -> b) -> Container a -> Container b
Container.prototype.map = function (f) {
  return Container.of(f(this.$value));
};

まさに配列のmapと似ていますが、[a]の代わりにContainer aを使います。動作原理は同じです:

js
Container.of(2).map(two => two + 2); 
// Container(4)

Container.of('flamethrowers').map(s => s.toUpperCase()); 
// Container('FLAMETHROWERS')

Container.of('bombs').map(append(' away')).map(prop('length')); 
// Container(10)

Containerから出ることなく値を操作できます。これが肝心です。コンテナ内の値はmap関数に渡され、加工後は安全にコンテナに戻されます。コンテナを離れないため、自由に関数をmapし続けられます。3番目の例のように、途中で型を変更することも可能です。

mapを呼び続けると、関数合成のように見えます。どんな数学的魔法が働いているのでしょう? 皆さん、これがファンクターです。

ファンクターはmapメソッドを実装し、特定の法則を遵守する型です

ファンクターとは契約付きのインターフェースです。Mappableと命名しても良かったのですが、fun要素が消えますね? カテゴリー論に由来し、数学的詳細は本章後半で扱います。今は直感と実用例を重視しましょう。

値を閉じ込めmapで操作する理由は? より良い問いを選べば答えが明らかになります: コンテナに関数を適用させる利点は? 関数適用の抽象化です。mapはコンテナ型に実行を委譲します。これは極めて強力な概念です。

シュレーディンガーのMaybe

cool cat, need reference

Containerは退屈です。実際はIdentityと呼ばれ、id関数と同程度の影響力しかありません(数学的関係は後述)。ただし、他にも有用なファンクター(適切なmapを持つコンテナ的な型)が存在します。

完全な実装は付録Bを参照

js
class Maybe {
  static of(x) {
    return new Maybe(x);
  }

  get isNothing() {
    return this.$value === null || this.$value === undefined;
  }

  constructor(x) {
    this.$value = x;
  }

  map(fn) {
    return this.isNothing ? this : Maybe.of(fn(this.$value));
  }

  inspect() {
    return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
  }
}

MaybeContainerに似ていますが、関数適用前に値の存在を確認します。これにより、map時にnull問題を回避できます(説明用に簡略化しています)。

js
Maybe.of('Malkovich Malkovich').map(match(/a/ig));
// Just(True)

Maybe.of(null).map(match(/a/ig));
// Nothing

Maybe.of({ name: 'Boris' }).map(prop('age')).map(add(10));
// Nothing

Maybe.of({ name: 'Dinah', age: 14 }).map(prop('age')).map(add(10));
// Just(24)

null値に関数をmapしてもエラーが発生しないことを確認してください。Maybeが毎回値の存在を確認するためです。

ドット構文は適切ですが、第1部で述べた理由からポイントフリースタイルを維持します。mapは任意のファンクターを委譲処理できるため:

js
// map :: Functor f => (a -> b) -> f a -> f b
const map = curry((f, anyFunctor) => anyFunctor.map(f));

通常の合成を継続でき、mapは期待通り動作します。Ramdaのmapも同様です。教育的な場合はドット記法、便利な場合はポイントフリーを使い分けます。型シグネチャに追加表記があることに気付きましたか? Functor f =>fがファンクターであることを示します。

ユースケース

実際のコードでは、結果を返せない可能性のある関数でMaybeを使用します。

js
// safeHead :: [a] -> Maybe(a)
const safeHead = xs => Maybe.of(xs[0]);

// streetName :: Object -> Maybe String
const streetName = compose(map(prop('street')), safeHead, prop('addresses'));

streetName({ addresses: [] });
// Nothing

streetName({ addresses: [{ street: 'Shady Ln.', number: 4201 }] });
// Just('Shady Ln.')

safeHeadは通常のheadに型安全性を追加したものです。Maybe導入により、null値の処理が強制されます。safeHeadは失敗の可能性を明示し、Maybeで包んで返します。値はMaybe内にあるため、mapで取得する必要があります。これはsafeHeadが強制するnullチェックです。予期せぬnull値の発生を防ぎ、堅牢なAPIを構築できます。

意図的にNothingを返す場合:

js
// withdraw :: Number -> Account -> Maybe(Account)
const withdraw = curry((amount, { balance }) =>
  Maybe.of(balance >= amount ? { balance: balance - amount } : null));

// This function is hypothetical, not implemented here... nor anywhere else.
// updateLedger :: Account -> Account 
const updateLedger = account => account;

// remainingBalance :: Account -> String
const remainingBalance = ({ balance }) => `Your balance is $${balance}`;

// finishTransaction :: Account -> String
const finishTransaction = compose(remainingBalance, updateLedger);


// getTwenty :: Account -> Maybe(String)
const getTwenty = compose(map(finishTransaction), withdraw(20));

getTwenty({ balance: 200.00 }); 
// Just('Your balance is $180')

getTwenty({ balance: 10.00 });
// Nothing

残高不足の場合withdrawNothingを返します。この失敗は明示的で、以降の処理はmapする必要があります。nullが意図的である点が異なり、計算が中断されます。残高更新や表示処理は、出金成功時のみ実行されます。

値の解放

重要な気付き: 最終的には効果を発揮する関数が必要です(JSON送信、画面表示、ファイルシステム操作など)。returnでは出力できず、関数を実行して外部へ送り出す必要があります。禅の公案のように「観測可能な効果のないプログラムは実行されるのか?」と。

アプリケーションの責務はデータの取得・変換・運搬です。最終的にコンテナから値を取り出さずにmapできれば十分です。Maybeから値を無理に取り出そうとするのは誤りです。シュレーディンガーの猫のように、値の状態は最終関数まで保持すべきです。これが論理分岐を伴わない直線的なフローを実現します。

脱出ハッチとしてmaybeヘルパーがあります。

js
// maybe :: b -> (a -> b) -> Maybe a -> b
const maybe = curry((v, f, m) => {
  if (m.isNothing) {
    return v;
  }

  return f(m.$value);
});

// getTwenty :: Account -> String
const getTwenty = compose(maybe('You\'re broke!', finishTransaction), withdraw(20));

getTwenty({ balance: 200.00 }); 
// 'Your balance is $180.00'

getTwenty({ balance: 10.00 }); 
// 'You\'re broke!'

静的値を返すか、Maybeなしで処理を継続します。maybeif/else文、mapif (x !== null) { return f(x) }に相当します。

Maybeの導入には慣れが必要です。SwiftやScalaのOption(al)に慣れた開発者は理解できるでしょう。nullチェックが煩わしく感じる場面もありますが、安全性向上のため習慣化すべきです。

安全でないソフトウェアは、卵をペステルで着色して車道に投げるようなもの。三匹の子豚が警告した材料で老人ホームを建てるようなものです。Maybeは安全性を高める強力な味方です。

実際の実装ではMaybeSome(x)/NoneまたはJust(x)/Nothingに分割します。これによりmapでのパラメトリシティを遵守し、nullundefinedも扱えます。

純粋なエラーハンドリング

pick a hand... need a reference

throw/catchは純粋ではありません。エラー発生時、出力値ではなく警報を発します! Eitherを使えば、戦闘的な入力拒否ではなく丁寧な応答が可能です。

完全な実装は付録Bを参照

js
class Either {
  static of(x) {
    return new Right(x);
  }

  constructor(x) {
    this.$value = x;
  }
}

class Left extends Either {
  map(f) {
    return this;
  }

  inspect() {
    return `Left(${inspect(this.$value)})`;
  }
}

class Right extends Either {
  map(f) {
    return Either.of(f(this.$value));
  }

  inspect() {
    return `Right(${inspect(this.$value)})`;
  }
}

const left = x => new Left(x);

LeftRightEitherのサブクラスです。親クラスは通常使用しませんが、存在を認識しておきます。動作を見てみましょう:

js
Either.of('rain').map(str => `b${str}`); 
// Right('brain')

left('rain').map(str => `It's gonna ${str}, better bring your umbrella!`); 
// Left('rain')

Either.of({ host: 'localhost', port: 80 }).map(prop('host'));
// Right('localhost')

left('rolls eyes...').map(prop('host'));
// Left('rolls eyes...')

Leftmapリクエストを無視します。RightContainer(つまりIdentity)のように動作します。Leftにエラーメッセージを埋め込める点が強力です。

誕生日から年齢計算する関数を考えます。Nothingで失敗を通知できますが、理由がわかりません。Eitherを使用する例です:

js
const moment = require('moment');

// getAge :: Date -> User -> Either(String, Number)
const getAge = curry((now, user) => {
  const birthDate = moment(user.birthDate, 'YYYY-MM-DD');

  return birthDate.isValid()
    ? Either.of(now.diff(birthDate, 'years'))
    : left('Birth date could not be parsed');
});

getAge(moment(), { birthDate: '2005-12-12' });
// Right(9)

getAge(moment(), { birthDate: 'July 4, 2001' });
// Left('Birth date could not be parsed')

Nothing同様、Left返却時は処理が停止します。違いは失敗理由がわかる点です。型シグネチャEither(String, Number)は文字列(左)か数値(右)を返します。厳密な実装ではありませんが、型情報から動作が推測できます。

js
// fortune :: Number -> String
const fortune = compose(concat('If you survive, you will be '), toString, add(1));

// zoltar :: User -> Either(String, _)
const zoltar = compose(map(console.log), map(fortune), getAge(moment()));

zoltar({ birthDate: '2005-12-12' });
// 'If you survive, you will be 10'
// Right(undefined)

zoltar({ birthDate: 'balloons!' });
// Left('Birth date could not be parsed')

有効な誕生日の場合、運勢を画面表示します。失敗時はLeftでエラーを保持します。エラーをthrowする代わりに、冷静にエラーを伝える方法です。

条件文の波括弧を登る代わりに、右から左への直線的なフローで分岐します。通常はconsole.logを外に出し、呼び出し時にmapしますが、Right分岐の違いを示すためこの実装とします。右側の型シグネチャ_は無視される値を表します。

重要な点:fortunefinishTransactionはファンクターを意識していません。呼び出し時にmapでラップすることで、非ファンクター関数をファンクター対応に昇格(lift)できます。この方式はコードの再利用性を高め、任意のファンクターに対応可能にします。

Eitherはバリデーションエラーから深刻なシステムエラーまで幅広く対応します。Maybeの例をEitherで置き換えてみてください。

Eitherを単なるエラーメッセージ容器として紹介したのは適切でないかもしれません。論理和(||)を型化したもので、カテゴリー論のコプロダクト概念も含みます(詳細は専門書を参照)。直和型として、取りうる値の数は包含する型の合計です。ファンクターとしてはエラーハンドリングに特化しています。

maybeと同様、eitherヘルパーもあります(静的値ではなく2つの関数を受け取ります):

js
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = curry((f, g, e) => {
  let result;

  switch (e.constructor) {
    case Left:
      result = f(e.$value);
      break;

    case Right:
      result = g(e.$value);
      break;

    // No Default
  }

  return result;
});

// zoltar :: User -> _
const zoltar = compose(console.log, either(id, fortune), getAge(moment()));

zoltar({ birthDate: '2005-12-12' });
// 'If you survive, you will be 10'
// undefined

zoltar({ birthDate: 'balloons!' });
// 'Birth date could not be parsed'
// undefined

謎のid関数が活躍します。Leftの値をconsole.logに渡す役割です。占いアプリの堅牢性が向上しました。ユーザーに厳しい現実を伝えるか、処理を継続します。次のファンクターへ進みましょう。

Old McDonaldは副作用を持っていた...

dominoes.. need a reference

純粋性に関する章で副作用を含む純粋関数の例を見ました。アクションを関数で包み込む手法です:

js
// getFromStorage :: String -> (_ -> String)
const getFromStorage = key => () => localStorage[key];

ラッパーがない場合、getFromStorageは外部要因で結果が変動します。堅牢なラッパーにより、常に同じ出力(特定のlocalStorage項目を取得する関数)を得ます。これで良心が痛むこともありません。

しかしこのままでは実用性に欠けます。未開封のフィギュアのように扱えません。コンテナ内の値にアクセスする方法は... IOの登場です。

js
class IO {
  static of(x) {
    return new IO(() => x);
  }

  constructor(fn) {
    this.$value = fn;
  }

  map(fn) {
    return new IO(compose(fn, this.$value));
  }

  inspect() {
    return `IO(${inspect(this.$value)})`;
  }
}

IOは他のファンクターと異なり、$valueが常に関数です。ただし実装詳細は無視し、包まれたアクションの戻り値を保持していると考えます。getFromStorageの例と同様、副作用を遅延実行します。of関数は評価を遅延させるためIO(() => x)とします(実質的にはIO(x))。表示上は仮想的な値を示しますが、実際に中身を確認するには効果を解放する必要があります。

使用例を見てみましょう:

js
// ioWindow :: IO Window
const ioWindow = new IO(() => window);

ioWindow.map(win => win.innerWidth);
// IO(1430)

ioWindow
  .map(prop('location'))
  .map(prop('href'))
  .map(split('/'));
// IO(['http:', '', 'localhost:8000', 'blog', 'posts'])


// $ :: String -> IO [DOM]
const $ = selector => new IO(() => document.querySelectorAll(selector));

$('#myDiv').map(head).map(div => div.innerHTML);
// IO('I am some inner html')

ioWindowは直接map可能なIOインスタンスです。`# 第8章: タッパーウェア

強力コンテナ

http://blog.dwinegar.com/2011/06/another-jar.html

これまで純粋関数を通じてデータを流すプログラムの書き方を学びました。これらは振る舞いの宣言的仕様です。しかし、制御フローやエラーハンドリング、非同期アクション、状態管理、そしてあえて言うなら副作用はどうでしょうか? 本章では、これら有用な抽象化の基盤を発見していきます。

まずコンテナを作成します。このコンテナはあらゆる型の値を保持できなければなりません。タピオカプリン専用のチャック付き袋など役に立ちません。オブジェクトとして実装しますが、オブジェクト指向的なプロパティやメソッドは与えません。宝物箱のように扱います──貴重なデータを優しく包み込む特別な箱です。

js
class Container {
  constructor(x) {
    this.$value = x;
  }
  
  static of(x) {
    return new Container(x);
  }
}

最初のコンテナです。思慮を込めてContainerと命名しました。newキーワードを頻繁に書かず済むよう、Container.ofをコンストラクタとして使用します。of関数には奥深い意味がありますが、現時点ではコンテナに値を安全に格納する適切な方法と考えてください。

新しく作った箱を調べてみましょう...

js
Container.of(3);
// Container(3)

Container.of('hotdogs');
// Container("hotdogs")

Container.of(Container.of({ name: 'yoda' }));
// Container(Container({ name: 'yoda' }))

Node.js環境ではContainer(x){$value: x}と表示されます。Chromeは型を正しく出力しますが、本質的には問題ありません。Containerの構造が理解できれば充分です。環境によってはinspectメソッドを上書きできますが、ここでは詳細に触れません。教育的・美的観点から、概念的な出力をinspectを上書きしたかのように記述します。

次に進む前に明確にしておきます:

  • Containerは1つのプロパティを持つオブジェクトです。多くのコンテナは単一の値を持ちますが、複数も可能です。プロパティ名は任意に$valueと命名しました。

  • $valueの型を特定の型に固定してはいけません。さもなくばContainerの名前に反します。

  • Containerに入ったデータは内部に保持されます。.$valueで取り出せますが、それは本来の目的を損ないます。

この設計の理由は後ほど明らかになりますが、今はしばらくお付き合いください。

ファンクター入門

コンテナ内の値(どんな型でも)に関数を適用する方法が必要です。

js
// (a -> b) -> Container a -> Container b
Container.prototype.map = function (f) {
  return Container.of(f(this.$value));
};

まさに配列のmapと似ていますが、[a]の代わりにContainer aを使います。動作原理は同じです:

js
Container.of(2).map(two => two + 2); 
// Container(4)

Container.of('flamethrowers').map(s => s.toUpperCase()); 
// Container('FLAMETHROWERS')

Container.of('bombs').map(append(' away')).map(prop('length')); 
// Container(10)

Containerから出ることなく値を操作できます。これが肝心です。コンテナ内の値はmap関数に渡され、加工後は安全にコンテナに戻されます。コンテナを離れないため、自由に関数をmapし続けられます。3番目の例のように、途中で型を変更することも可能です。

mapを呼び続けると、関数合成のように見えます。どんな数学的魔法が働いているのでしょう? 皆さん、これがファンクターです。

ファンクターはmapメソッドを実装し、特定の法則を遵守する型です

ファンクターとは契約付きのインターフェースです。Mappableと命名しても良かったのですが、fun要素が消えますね? カテゴリー論に由来し、数学的詳細は本章後半で扱います。今は直感と実用例を重視しましょう。

値を閉じ込めmapで操作する理由は? より良い問いを選べば答えが明らかになります: コンテナに関数を適用させる利点は? 関数適用の抽象化です。mapはコンテナ型に実行を委譲します。これは極めて強力な概念です。

シュレーディンガーのMaybe

cool cat, need reference

Containerは退屈です。実際はIdentityと呼ばれ、id関数と同程度の影響力しかありません(数学的関係は後述)。ただし、他にも有用なファンクター(適切なmapを持つコンテナ的な型)が存在します。

完全な実装は付録Bを参照

js
class Maybe {
  static of(x) {
    return new Maybe(x);
  }

  get isNothing() {
    return this.$value === null || this.$value === undefined;
  }

  constructor(x) {
    this.$value = x;
  }

  map(fn) {
    return this.isNothing ? this : Maybe.of(fn(this.$value));
  }

  inspect() {
    return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
  }
}

MaybeContainerに似ていますが、関数適用前に値の存在を確認します。これにより、map時にnull問題を回避できます(説明用に簡略化しています)。

js
Maybe.of('Malkovich Malkovich').map(match(/a/ig));
// Just(True)

Maybe.of(null).map(match(/a/ig));
// Nothing

Maybe.of({ name: 'Boris' }).map(prop('age')).map(add(10));
// Nothing

Maybe.of({ name: 'Dinah', age: 14 }).map(prop('age')).map(add(10));
// Just(24)

null値に関数をmapしてもエラーが発生しないことを確認してください。Maybeが毎回値の存在を確認するためです。

ドット構文は適切ですが、第1部で述べた理由からポイントフリースタイルを維持します。mapは任意のファンクターを委譲処理できるため:

js
// map :: Functor f => (a -> b) -> f a -> f b
const map = curry((f, anyFunctor) => anyFunctor.map(f));

通常の合成を継続でき、mapは期待通り動作します。Ramdaのmapも同様です。教育的な場合はドット記法、便利な場合はポイントフリーを使い分けます。型シグネチャに追加表記があることに気付きましたか? Functor f =>fがファンクターであることを示します。

ユースケース

実際のコードでは、結果を返せない可能性のある関数でMaybeを使用します。

js
// safeHead :: [a] -> Maybe(a)
const safeHead = xs => Maybe.of(xs[0]);

// streetName :: Object -> Maybe String
const streetName = compose(map(prop('street')), safeHead, prop('addresses'));

streetName({ addresses: [] });
// Nothing

streetName({ addresses: [{ street: 'Shady Ln.', number: 4201 }] });
// Just('Shady Ln.')

safeHeadは通常のheadに型安全性を追加したものです。Maybe導入により、null値の処理が強制されます。safeHeadは失敗の可能性を明示し、Maybeで包んで返します。値はMaybe内にあるため、mapで取得する必要があります。これはsafeHeadが強制するnullチェックです。予期せぬnull値の発生を防ぎ、堅牢なAPIを構築できます。

意図的にNothingを返す場合:

js
// withdraw :: Number -> Account -> Maybe(Account)
const withdraw = curry((amount, { balance }) =>
  Maybe.of(balance >= amount ? { balance: balance - amount } : null));

// This function is hypothetical, not implemented here... nor anywhere else.
// updateLedger :: Account -> Account 
const updateLedger = account => account;

// remainingBalance :: Account -> String
const remainingBalance = ({ balance }) => `Your balance is $${balance}`;

// finishTransaction :: Account -> String
const finishTransaction = compose(remainingBalance, updateLedger);


// getTwenty :: Account -> Maybe(String)
const getTwenty = compose(map(finishTransaction), withdraw(20));

getTwenty({ balance: 200.00 }); 
// Just('Your balance is $180')

getTwenty({ balance: 10.00 });
// Nothing

残高不足の場合withdrawNothingを返します。この失敗は明示的で、以降の処理はmapする必要があります。nullが意図的である点が異なり、計算が中断されます。残高更新や表示処理は、出金成功時のみ実行されます。

値の解放

重要な気付き: 最終的には効果を発揮する関数が必要です(JSON送信、画面表示、ファイルシステム操作など)。returnでは出力できず、関数を実行して外部へ送り出す必要があります。禅の公案のように「観測可能な効果のないプログラムは実行されるのか?」と。

アプリケーションの責務はデータの取得・変換・運搬です。最終的にコンテナから値を取り出さずにmapできれば十分です。Maybeから値を無理に取り出そうとするのは誤りです。シュレーディンガーの猫のように、値の状態は最終関数まで保持すべきです。これが論理分岐を伴わない直線的なフローを実現します。

脱出ハッチとしてmaybeヘルパーがあります。

js
// maybe :: b -> (a -> b) -> Maybe a -> b
const maybe = curry((v, f, m) => {
  if (m.isNothing) {
    return v;
  }

  return f(m.$value);
});

// getTwenty :: Account -> String
const getTwenty = compose(maybe('You\'re broke!', finishTransaction), withdraw(20));

getTwenty({ balance: 200.00 }); 
// 'Your balance is $180.00'

getTwenty({ balance: 10.00 }); 
// 'You\'re broke!'

静的値を返すか、Maybeなしで処理を継続します。maybeif/else文、mapif (x !== null) { return f(x) }に相当します。

Maybeの導入には慣れが必要です。SwiftやScalaのOption(al)に慣れた開発者は理解できるでしょう。nullチェックが煩わしく感じる場面もありますが、安全性向上のため習慣化すべきです。

安全でないソフトウェアは、卵をペステルで着色して車道に投げるようなもの。三匹の子豚が警告した材料で老人ホームを建てるようなものです。Maybeは安全性を高める強力な味方です。

実際の実装ではMaybeSome(x)/NoneまたはJust(x)/Nothingに分割します。これによりmapでのパラメトリシティを遵守し、nullundefinedも扱えます。

純粋なエラーハンドリング

pick a hand... need a reference

throw/catchは純粋ではありません。エラー発生時、出力値ではなく警報を発します! Eitherを使えば、戦闘的な入力拒否ではなく丁寧な応答が可能です。

完全な実装は付録Bを参照

js
class Either {
  static of(x) {
    return new Right(x);
  }

  constructor(x) {
    this.$value = x;
  }
}

class Left extends Either {
  map(f) {
    return this;
  }

  inspect() {
    return `Left(${inspect(this.$value)})`;
  }
}

class Right extends Either {
  map(f) {
    return Either.of(f(this.$value));
  }

  inspect() {
    return `Right(${inspect(this.$value)})`;
  }
}

const left = x => new Left(x);

LeftRightEitherのサブクラスです。親クラスは通常使用しませんが、存在を認識しておきます。動作を見てみましょう:

js
Either.of('rain').map(str => `b${str}`); 
// Right('brain')

left('rain').map(str => `It's gonna ${str}, better bring your umbrella!`); 
// Left('rain')

Either.of({ host: 'localhost', port: 80 }).map(prop('host'));
// Right('localhost')

left('rolls eyes...').map(prop('host'));
// Left('rolls eyes...')

Leftmapリクエストを無視します。RightContainer(つまりIdentity)のように動作します。Leftにエラーメッセージを埋め込める点が強力です。

誕生日から年齢計算する関数を考えます。Nothingで失敗を通知できますが、理由がわかりません。Eitherを使用する例です:

js
const moment = require('moment');

// getAge :: Date -> User -> Either(String, Number)
const getAge = curry((now, user) => {
  const birthDate = moment(user.birthDate, 'YYYY-MM-DD');

  return birthDate.isValid()
    ? Either.of(now.diff(birthDate, 'years'))
    : left('Birth date could not be parsed');
});

getAge(moment(), { birthDate: '2005-12-12' });
// Right(9)

getAge(moment(), { birthDate: 'July 4, 2001' });
// Left('Birth date could not be parsed')

Nothing同様、Left返却時は処理が停止します。違いは失敗理由がわかる点です。型シグネチャEither(String, Number)は文字列(左)か数値(右)を返します。厳密な実装ではありませんが、型情報から動作が推測できます。

js
// fortune :: Number -> String
const fortune = compose(concat('If you survive, you will be '), toString, add(1));

// zoltar :: User -> Either(String, _)
const zoltar = compose(map(console.log), map(fortune), getAge(moment()));

zoltar({ birthDate: '2005-12-12' });
// 'If you survive, you will be 10'
// Right(undefined)

zoltar({ birthDate: 'balloons!' });
// Left('Birth date could not be parsed')

有効な誕生日の場合、運勢を画面表示します。失敗時はLeftでエラーを保持します。エラーをthrowする代わりに、冷静にエラーを伝える方法です。

条件文の波括弧を登る代わりに、右から左への直線的なフローで分岐します。通常はconsole.logを外に出し、呼び出し時にmapしますが、Right分岐の違いを示すためこの実装とします。右側の型シグネチャ_は無視される値を表します。

重要な点:fortunefinishTransactionはファンクターを意識していません。呼び出し時にmapでラップすることで、非ファンクター関数をファンクター対応に昇格(lift)できます。この方式はコードの再利用性を高め、任意のファンクターに対応可能にします。

Eitherはバリデーションエラーから深刻なシステムエラーまで幅広く対応します。Maybeの例をEitherで置き換えてみてください。

Eitherを単なるエラーメッセージ容器として紹介したのは適切でないかもしれません。論理和(||)を型化したもので、カテゴリー論のコプロダクト概念も含みます(詳細は専門書を参照)。直和型として、取りうる値の数は包含する型の合計です。ファンクターとしてはエラーハンドリングに特化しています。

maybeと同様、eitherヘルパーもあります(静的値ではなく2つの関数を受け取ります):

js
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = curry((f, g, e) => {
  let result;

  switch (e.constructor) {
    case Left:
      result = f(e.$value);
      break;

    case Right:
      result = g(e.$value);
      break;

    // No Default
  }

  return result;
});

// zoltar :: User -> _
const zoltar = compose(console.log, either(id, fortune), getAge(moment()));

zoltar({ birthDate: '2005-12-12' });
// 'If you survive, you will be 10'
// undefined

zoltar({ birthDate: 'balloons!' });
// 'Birth date could not be parsed'
// undefined

謎のid関数が活躍します。Leftの値をconsole.logに渡す役割です。占いアプリの堅牢性が向上しました。ユーザーに厳しい現実を伝えるか、処理を継続します。次のファンクターへ進みましょう。

Old McDonaldは副作用を持っていた...

dominoes.. need a reference

純粋性に関する章で副作用を含む純粋関数の例を見ました。アクションを関数で包み込む手法です:

js
// getFromStorage :: String -> (_ -> String)
const getFromStorage = key => () => localStorage[key];

ラッパーがない場合、getFromStorageは外部要因で結果が変動します。堅牢なラッパーにより、常に同じ出力(特定のlocalStorage項目を取得する関数)を得ます。これで良心が痛むこともありません。

しかしこのままでは実用性に欠けます。未開封のフィギュアのように扱えません。コンテナ内の値にアクセスする方法は... IOの登場です。

js
class IO {
  static of(x) {
    return new IO(() => x);
  }

  constructor(fn) {
    this.$value = fn;
  }

  map(fn) {
    return new IO(compose(fn, this.$value));
  }

  inspect() {
    return `IO(${inspect(this.$value)})`;
  }
}

IOは他のファンクターと異なり、$valueが常に関数です。ただし実装詳細は無視し、包まれたアクションの戻り値を保持していると考えます。getFromStorageの例と同様、副作用を遅延実行します。of関数は評価を遅延させるためIO(() => x)とします(実質的にはIO(x))。表示上は仮想的な値を示しますが、実際に中身を確認するには効果を解放する必要があります。

使用例を見てみましょう:

js
// ioWindow :: IO Window
const ioWindow = new IO(() => window);

ioWindow.map(win => win.innerWidth);
// IO(1430)

ioWindow
  .map(prop('location'))
  .map(prop('href'))
  .map(split('/'));
// IO(['http:', '', 'localhost:8000', 'blog', 'posts'])


// $ :: String -> IO [DOM]
const $ = selector => new IO(() => document.querySelectorAll(selector));

$('#myDiv').map(head).map(div => div.innerHTML);
// IO('I am some inner html')

は呼び出し後にIOを返す関数です。概念的な戻り値は理解用のもので、実際は{ $value: [Function] }です。map時に関数を合成チェーンの末尾に追加し、計算を構築します。ドミノ倒しのように実行を遅延させるパターンです。

ファンクターの直感を働かせてください。実装詳細を超え、あらゆるコンテナをmapできる感覚が重要です。この心理的感覚はファンクター則に由来し、本章後半で検証します。純粋性を保ちながら副作用を扱えるようになりました。

しかし最終的には副作用を解放する必要があります。IOの計算構築後、いつ・どこで実行すべきか? 呼び出し元に責任を委譲すれば、純粋性を維持できます。具体例です:

js
// url :: IO String
const url = new IO(() => window.location.href);

// toPairs :: String -> [[String]]
const toPairs = compose(map(split('=')), split('&'));

// params :: String -> [[String]]
const params = compose(toPairs, last, split('?'));

// findParam :: String -> IO Maybe [String]
const findParam = key => map(compose(Maybe.of, find(compose(eq(key), head)), params), url);

// -- Impure calling code ----------------------------------------------

// run it by calling $value()!
findParam('searchTerm').$value();
// Just(['searchTerm', 'wafflehouse'])

ライブラリはurlIOで包み、呼び出し元に実行を任せます。IO(Maybe([x]))のようにファンクターを多重にすることも可能で、表現力が豊かです。

懸念事項: IO$valueは本来の値ではなく、外部からアクセス可能です。プロパティ名をunsafePerformIOに変更し、危険性を明示します。

js
class IO {
  constructor(io) {
    this.unsafePerformIO = io;
  }

  map(fn) {
    return new IO(compose(fn, this.unsafePerformIO));
  }
}

呼び出し側はfindParam('searchTerm').unsafePerformIO()で明確になります。

IOは副作用を制御する強力な味方です。次は用途の異なる型を見ていきます。

非同期タスク

コールバックは地獄への螺旋階段です。M.C.エッシャーが設計した制御フローのよう。波括弧のジャングルジムで窒息しそうです。非同期処理には「F」で始まるより良い方法があります。

内部実装は複雑なため、FolktaleのData.Task(旧Data.Future)を使用します。使用例:

js
// -- Node readFile example ------------------------------------------

const fs = require('fs');

// readFile :: String -> Task Error String
const readFile = filename => new Task((reject, result) => {
  fs.readFile(filename, (err, data) => (err ? reject(err) : result(data)));
});

readFile('metamorphosis').map(split('\n')).map(head);
// Task('One morning, as Gregor Samsa was waking up from anxious dreams, he discovered that
// in bed he had been changed into a monstrous verminous bug.')


// -- jQuery getJSON example -----------------------------------------

// getJSON :: String -> {} -> Task Error JSON
const getJSON = curry((url, params) => new Task((reject, result) => {
  $.getJSON(url, params, result).fail(reject);
}));

getJSON('/video', { id: 10 }).map(prop('title'));
// Task('Family Matters ep 15')


// -- Default Minimal Context ----------------------------------------

// We can put normal, non futuristic values inside as well
Task.of(3).map(three => three + 1);
// Task(4)

rejectresultはエラー/成功コールバックです。mapを使って未来の値を操作します。mapはお馴染みですね。

Promiseに慣れている方はmapthenTaskをPromiseと見做せます。Promiseは純粋ではありませんが、類似性があります。

IO同様Taskは実行命令を待機します。実際、非同期処理ではIOTaskに包含されます。readFilegetJSONIOは不要です。mapは未来の指示を積み重ねるタイムカプセルのようなもの。洗練された先延ばし技術です。

Task実行にはforkメソッドを使用します。unsafePerformIOと似ていますが、非ブロッキング処理です。イベントループは回り続けます:

js
// -- Pure application -------------------------------------------------
// blogPage :: Posts -> HTML
const blogPage = Handlebars.compile(blogTemplate);

// renderPage :: Posts -> HTML
const renderPage = compose(blogPage, sortBy(prop('date')));

// blog :: Params -> Task Error HTML
const blog = compose(map(renderPage), getJSON('/posts'));


// -- Impure calling code ----------------------------------------------
blog({}).fork(
  error => $('#error').html(error.message),
  page => $('#main').html(page),
);

$('#spinner').show();

fork呼び出し後、Taskは投稿を取得しページを表示します。待機中はスピナーを表示します。成功時はページ表示、失敗時はエラーを表示します。

制御フローの直線性に注目してください。実際の実行順序に関わらず、コードを上から下、右から左に読めます。コールバック地獄より理解しやすい構造です。

TaskEitherも内包します! 非同期世界での失敗処理に必要です。純粋なエラーハンドリングを提供します。

Task導入後もIOEitherは活躍します。複雑ですが示唆に富む例です:

js
// Postgres.connect :: Url -> IO DbConnection
// runQuery :: DbConnection -> ResultSet
// readFile :: String -> Task Error String

// -- Pure application -------------------------------------------------

// dbUrl :: Config -> Either Error Url
const dbUrl = ({ uname, pass, host, db }) => {
  if (uname && pass && host && db) {
    return Either.of(`db:pg://${uname}:${pass}@${host}5432/${db}`);
  }

  return left(Error('Invalid config!'));
};

// connectDb :: Config -> Either Error (IO DbConnection)
const connectDb = compose(map(Postgres.connect), dbUrl);

// getConfig :: Filename -> Task Error (Either Error (IO DbConnection))
const getConfig = compose(map(compose(connectDb, JSON.parse)), readFile);


// -- Impure calling code ----------------------------------------------

getConfig('db.json').fork(
  logErr('couldn\'t read file'),
  either(console.log, map(runQuery)),
);

非同期ファイル読み取りはTask、設定検証はEither、DB接続はIOが担当します。同期的処理では依然として有用です。

これで基本的な概念は網羅しました。mapのシンプルさが全てです。

実際には複数の非同期タスクを扱う必要がありますが、現時点のコンテナAPIでは不十分です。モナドの説明へ進む前に、数学的基盤を確認します。

理論の一考察

前述の通り、ファンクターはカテゴリー論に基づき法則を満たします。まず有用な特性を見ていきましょう。

js
// identity
map(id) === id;

// composition
compose(map(f), map(g)) === map(compose(f, g));

*恒等(identity)*法則は単純ながら重要です。実行可能なコードで検証できます:

js
const idLaw1 = map(id);
const idLaw2 = id;

idLaw1(Container.of(2)); // Container(2)
idLaw2(Container.of(2)); // Container(2)

両者は等価です。次に合成則を確認します。

js
const compLaw1 = compose(map(append(' world')), map(append(' cruel')));
const compLaw2 = map(compose(append(' world'), append(' cruel')));

compLaw1(Container.of('Goodbye')); // Container('Goodbye cruel world')
compLaw2(Container.of('Goodbye')); // Container('Goodbye cruel world')

カテゴリー論では、ファンクターは対象と射を別カテゴリーへマップします。新しいカテゴリーは恒等射と射の合成を保持する必要がありますが、前述の法則がこれを保証します。

カテゴリーを対象のネットワークと射の接続と捉えてください。ファンクターFは元のカテゴリーCの対象aF aとしてDへマップします。図解:

Categories mapped

例えばMaybeは、型と関数のカテゴリーを「値が存在しない可能性あり」「nullチェック付き射」のカテゴリーへマップします。mapで関数を包み、型をファンクター化します。エンドファンクターとして元のカテゴリーのサブカテゴリーを作ります。

ファンクターFによる射のマッピング図解:

functor diagram

図の通り、射の各経路は同じ結果を生成します(可換図式)。数式に基づくコードの推論が可能になります。具体例:

js
// topRoute :: String -> Maybe String
const topRoute = compose(Maybe.of, reverse);

// bottomRoute :: String -> Maybe String
const bottomRoute = compose(map(reverse), Maybe.of);

topRoute('hi'); // Just('ih')
bottomRoute('hi'); // Just('ih')

視覚的表現:

functor diagram 2

ファンクターの特性に基づき、コードを即座に解析・リファクタリング可能です。

ファンクターは積み重ね可能:

js
const nested = Task.of([Either.of('pillows'), left('no sleep for you')]);

map(map(map(toUpperCase)), nested);
// Task([Right('PILLOWS'), Left('no sleep for you')])

nestedはエラーの可能性がある未来の配列です。各層をmapで剥がし関数を適用します。コールバックや条件分岐なしで文脈を明示します。ただしmap(map(map(f)))が必要です。代わりにファンクターを合成できます。

js
class Compose {
  constructor(fgx) {
    this.getCompose = fgx;
  }

  static of(fgx) {
    return new Compose(fgx);
  }

  map(fn) {
    return new Compose(map(map(fn), this.getCompose));
  }
}

const tmd = Task.of(Maybe.of('Rock over London'));

const ctmd = Compose.of(tmd);

const ctmd2 = map(append(', rock on, Chicago'), ctmd);
// Compose(Task(Just('Rock over London, rock on, Chicago')))

ctmd2.getCompose;
// Task(Just('Rock over London, rock on, Chicago'))

composeで1回のmapに! ファンクター合成は結合的で、Container(Identityファンクター)の存在によりカテゴリーを形成します。

まとめ

いくつかのファンクターを見ましたが、他にも木構造、リスト、マップなど無限に存在します。イベントストリームやObservableもファンクターです。ファンクターは至る所にあり、本書で広く利用します。

複数のファンクター引数を扱う方法や、非同期アクションの順序制御は? これらの課題にはモナドが必要です。次章で直撃します。

第9章: モナディック・オニオン

演習問題

練習開始!

addmapを使って、ファンクター内の値を増加させる関数を作成せよ // incrF :: Functor f => f Int -> f Int const incrF = undefined;


以下のUserオブジェクトが与えられた時:

js
const user = { id: 2, name: 'Albert', active: true };

練習開始!

safePropheadを使用してユーザーのイニシャルを取得せよ // initial :: User -> Maybe String const initial = undefined;


以下のヘルパー関数が与えられた時:

js
// showWelcome :: User -> String
const showWelcome = compose(concat('Welcome '), prop('name'));

// checkActive :: User -> Either String User
const checkActive = function checkActive(user) {
  return user.active
    ? Either.of(user)
    : left('Your account is not active');
};

練習開始!

checkActiveshowWelcomeを使い、アクセス許可を付与またはエラーを返す関数を作成せよ // eitherWelcome :: User -> Either String String const eitherWelcome = undefined;


以下の関数を考慮する:

js
// validateUser :: (User -> Either String ()) -> User -> Either String User
const validateUser = curry((validate, user) => validate(user).map(_ => user));

// save :: User -> IO User
const save = user => new IO(() => ({ ...user, saved: true }));

練習開始!

ユーザー名が3文字以上か検証するvalidateName関数、および検証成功時にeithershowWelcomesaveを使ってユーザー登録を行うregister関数を作成せよ eitherの2つの引数は同じ型を返す必要がある // validateName :: User -> Either String () const validateName = undefined; // register :: User -> IO String const register = compose(undefined, validateUser(validateName));