Skip to content

第10章 アプリカティブ函手

アプリカティブの適用

applicative functor(アプリカティブ函手)の名称は、その関数型プログラミングの起源を反映して的確に命名されています。関数型プログラマーはmappendliftA4のような命名で悪名高い存在ですが、数学的な文脈では自然に見えるこれらの名称も、他の状況では判断に迷うダース・ベイダーの如く曖昧さを帯びるのです。

このインターフェースが提供するものは名称から推察可能です:函手同士を相互に適用する能力を。

では、まともな理性を持つ我々がなぜこのような機能を求めるのでしょう?そもそも函手同士を適用するとはどういう意味なのでしょうか?

この問いに答えるため、関数型プログラミング経験者なら既に遭遇したことがある状況を考えます。具体的には、同じ型の2つの函手があり、両方の値を引数として関数を呼び出したい場合です。例えば2つのContainerの値を加算するような単純な処理です。

js
// We can't do this because the numbers are bottled up.
add(Container.of(2), Container.of(3));
// NaN

// Let's use our trusty map
const containerOfAdd2 = map(add, Container.of(2));
// Container(add(2))

部分適用された関数を含むContainerがある状況を想定します。より具体的にはContainer(add(2))があり、内部のadd(2)Container(3)の値3に適用したい場合です。つまり、函手同士を適用したいということになります。

実はこの課題を達成するためのツールは既に揃っています。次のようにchainmapを用いて部分適用されたadd(2)を処理できます:

js
Container.of(2).chain(two => Container.of(3).map(add(two)));

ここでの問題は、前のモナドの処理が完了するまで次の評価ができないモナドの逐次処理世界に制約されている点です。2つの独立した強固な値がある場合、モナドの逐次処理要件を満たすためだけにContainer(3)の生成を遅延させる必要はありません。

実際、このような状況では不要な関数や変数を使わずに簡潔に函手の内容を相互適用できれば理想的です。

瓶の中の船

https://www.deviantart.com/hollycarden

apはある函手に含まれる関数を別の函手の値に適用できる関数です。早口で5回言ってみてください。

js
Container.of(add(2)).ap(Container.of(3));
// Container(5)

// all together now

Container.of(2).map(add).ap(Container.of(3));
// Container(5)

このように簡潔に表現できます。重要な点として、addがカリー化されている場合のみ機能するため、最初のmap処理でaddが部分適用されます。

apは次のように定義可能です:

js
Container.prototype.ap = function (otherContainer) {
  return otherContainer.map(this.$value);
};

this.$valueが関数であり、別の函手を受け取るためmapする必要があることに注意してください。これにより次のインターフェース定義が得られます:

アプリカティブ函手とはapメソッドを持つポイント付き函手である

ポイント付きへの依存性に注目してください。以下の例でわかるように、ポイント付きインターフェースが重要となります。

若干の懐疑的な気持ち(あるいは困惑や恐怖)を覚えるかもしれませんが、心を開いてください。このapは有用であることを証明します。本題に入る前に、興味深い特性を検討しましょう。

js
F.of(x).map(f) === F.of(f).ap(F.of(x));

正確に言えば、fのマッピングはfの函手をapすることと等価です。あるいは、xをコンテナに格納してmap(f)するか、fxの両方をコンテナに持ち上げてapすることが可能です。これにより左から右への記述が可能になります:

js
Maybe.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
// Maybe(5)

Task.of(add).ap(Task.of(2)).ap(Task.of(3));
// Task(5)

半眼で見れば通常の関数呼び出しの形に見えるかもしれません。ポイントフリー版については後述しますが、現時点ではこの記法が推奨されます。ofを使用することで各値がコンテナの魔法世界へ運ばれ、この並行宇宙では非同期処理やnull処理など様々な状況下でapが関数を適用します。まさにビンの中の船を建造するようなものです。

例でTaskを使用したことに気付きましたか?これこそアプリカティブ函手が真価を発揮する典型的な状況です。より詳細な例を見てみましょう。

協調動作の動機

旅行サイトを構築し、観光地リストと地域イベントを取得したいとします。これらは別々のAPI呼び出しです。

js
// Http.get :: String -> Task Error HTML

const renderPage = curry((destinations, events) => { /* render page */ });

Task.of(renderPage).ap(Http.get('/destinations')).ap(Http.get('/events'));
// Task("<div>some page with dest and events</div>")

両方のHttp呼び出しは即座に実行され、両方が解決した時点でrenderPageが呼び出されます。モナド版と異なり、一方のTaskが完了してから次の処理が始まる必要がありません。目的地情報がイベント取得に不要なため、逐次評価から解放されます。

再び部分適用を使用しているため、renderPageがカリー化されている必要があります。手動で実装した経験があれば、このインターフェースの驚くべき単純さに感謝するでしょう。これは特異点へ近づく美しいコードです。

別の例を見てみましょう。

js
// $ :: String -> IO DOM
const $ = selector => new IO(() => document.querySelector(selector));

// getVal :: String -> IO String
const getVal = compose(map(prop('value')), $);

// signIn :: String -> String -> Bool -> User
const signIn = curry((username, password, rememberMe) => { /* signing in */ });

IO.of(signIn).ap(getVal('#email')).ap(getVal('#password')).ap(IO.of(false));
// IO({ id: 3, email: 'gg@allin.com' })

signInは3引数のカリー化関数であるため、適切にapを適用する必要があります。各apごとにsignInに引数が渡され、完了次第実行されます。引数の数に応じてこのパターンを拡張可能です。また、最後の引数はapが同じ型を要求するためofによる持ち上げが必要です。

アプリカティブの『持ち上げ』術

アプリカティブ呼び出しのポイントフリー記法を検討します。mapof/apと等価であるため、指定回数分apを実行する汎用関数を記述可能です:

js
const liftA2 = curry((g, f1, f2) => f1.map(g).ap(f2));

const liftA3 = curry((g, f1, f2, f3) => f1.map(g).ap(f2).ap(f3));

// liftA4, etc

liftA2は風変わりな名称です。老朽化した工場の気難しい貨物用エレベータや安価なリムジン会社の社名プレートを想起させます。しかし本質を理解すれば自明です:アプリカティブ函手世界へ要素を持ち上げるのです。

この2-3-4の不自然さは当初醜く不要に見えました。JavaScriptでは関数のアリティを動的に確認可能ですが、部分的にliftA(N)を適用できるため引数数の多様化は不可能です。

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

js
// checkEmail :: User -> Either String Email
// checkName :: User -> Either String String

const user = {
  name: 'John Doe',
  email: 'blurp_blurp',
};

//  createUser :: Email -> String -> IO User
const createUser = curry((email, name) => { /* creating... */ });

Either.of(createUser).ap(checkEmail(user)).ap(checkName(user));
// Left('invalid email')

liftA2(createUser, checkEmail(user), checkName(user));
// Left('invalid email')

createUserが2引数を取るため、対応するliftA2を使用します。両記述は等価ですが、liftA2版ではEitherに言及されていません。これにより特定の型に依存しない汎用的で柔軟な実装が可能です。

以前の例をこのスタイルで書き直してみます:

js
liftA2(add, Maybe.of(2), Maybe.of(3));
// Maybe(5)

liftA2(renderPage, Http.get('/destinations'), Http.get('/events'));
// Task('<div>some page with dest and events</div>')

liftA3(signIn, getVal('#email'), getVal('#password'), IO.of(false));
// IO({ id: 3, email: 'gg@allin.com' })

演算子

Haskell、Scala、PureScript、Swiftなどの独自中置演算子が定義可能な言語では次の構文を見かけます:

hs
-- Haskell / PureScript
add <$> Right 2 <*> Right 3
js
// JavaScript
map(add, Right(2)).ap(Right(3));

<$>mapfmap)、<*>apであることを覚えておきましょう。より自然な関数適用スタイルを可能にし、括弧を削減します。

無料の缶切り

http://www.breannabeckmeyer.com/

派生関数についてはほとんど言及していません。すべてのインターフェースが相互に構築され一連の法則に従うため、強力なインターフェースから弱いインターフェースを定義可能です。

例えば、アプリカティブはまず函手であるため、アプリカティブインスタンスが存在すれば函手を定義可能です。

この種の完璧な計算の調和は数学的フレームワーク内で作業しているため可能です。モーツァルトが少年期にAbletonを違法ダウンロードしてもこれほどの調和を生み出せなかったでしょう。

of/apmapと等価であると前述しました。この知識を活用してmapを無料で定義可能です:

js
// map derived from of/ap
X.prototype.map = function map(f) {
  return this.constructor.of(f).ap(this);
};

モナドは食物連鎖の頂点に位置します。つまりchainがあれば函手とアプリカティブを無料で取得可能です:

js
// map derived from chain
X.prototype.map = function map(f) {
  return this.chain(a => this.constructor.of(f(a)));
};

// ap derived from chain/map
X.prototype.ap = function ap(other) {
  return this.chain(f => other.map(f));
};

モナドを定義可能であれば、アプリカティブと函手指向インターフェースを定義可能です。型を検査してこのプロセスを自動化できるのは注目に値します。

apの魅力の一部は並列実行能力にあるため、chain経由の定義では最適化を逃すことに留意が必要です。しかし、最適実装を見つけるまでの暫定インターフェースとして有用です。

なぜモナドだけで済ませないのか?必要最小限の機能層で作業することが認知負荷を軽減します。この観点から、モナドよりアプリカティブを優先するのが良い作法です。

モナドは計算の逐次処理、変数割当、ネスト構造による実行停止が可能な独自能力を持ちます。アプリカティブ使用時にはこれらの懸念から解放されます。

それでは法的根拠について...

法則

他の数学構造と同様、アプリカティブ函手は日々のコーディングで頼りになる有用な特性を保持します。まず、アプリカティブは「合成について閉じている」ため、apがコンテナ型を変更することはありません(モナドより優位な理由)。複数の異なる効果を組み合わせ可能で、アプリケーション全体を通じて型が維持されます。

具体例:

js
const tOfM = compose(Task.of, Maybe.of);

liftA2(liftA2(concat), tOfM('Rainy Days and Mondays'), tOfM(' always get me down'));
// Task(Maybe(Rainy Days and Mondays always get me down))

異なる型が混在する心配は不要です。

重要な法則である同一性を見てみましょう:

同一性

js
// identity
A.of(id).ap(v) === v;

函手内のidを値vに適用しても結果は不変です。例えば:

js
const v = Identity.of('Pillow Pets');
Identity.of(id).ap(v) === v;

Identity.of(id)の無力さに苦笑いします。重要な点は前述の通りof/apmapと等価であるため、この法則が函手同一性map(id) == idから直接導かれることです。

これらの法則を使用する利点は、軍事的な幼稚園体育教師のように全てのインターフェースを適切に連携させる点にあります。

準同型

js
// homomorphism
A.of(f).ap(A.of(x)) === A.of(f(x));

準同型とは構造保存写像のことです。実際、函手はカテゴリ間の準同型であり、元のカテゴリ構造を保存します。

通常の関数と値をコンテナに格納し、内部で計算を実行するため、コンテナ内全体を適用(等式左辺)した場合と外部で適用後に格納(右辺)した場合が等しくなるのは自然です。

簡単な例:

js
Either.of(toUpperCase).ap(Either.of('oreos')) === Either.of(toUpperCase('oreos'));

交換則

交換則apの左右どちらに関数を持ち上げても結果が変わらないことを保証します。

js
// interchange
v.ap(A.of(x)) === A.of(f => f(x)).ap(v);

例を示します:

js
const v = Task.of(reverse);
const x = 'Sparklehorse';

v.ap(Task.of(x)) === Task.of(f => f(x)).ap(v);

合成

最後に合成則です。これはコンテナ内での適用時に関数合成が保持されることを検証します。

js
// composition
A.of(compose).ap(u).ap(v).ap(w) === u.ap(v.ap(w));
js
const u = IO.of(toUpperCase);
const v = IO.of(concat('& beyond'));
const w = IO.of('blood bath ');

IO.of(compose).ap(u).ap(v).ap(w) === u.ap(v.ap(w));

まとめ

アプリカティブの有用なユースケースは複数の函引数がある場合です。函手世界内で関数を引数に適用可能にする能力を提供します。モナドでも可能ですが、モナド固有機能が必要ない場合はアプリカティブ函手を優先すべきです。

コンテナAPIについてほぼ解説完了しました。mapchainapの使用方法を学びました。次章では複数の函手を体系的に分解し効果的に扱う方法を学びます。

第11章: 再び自然変換へ

演習問題

練習開始!

Maybeapを使用してnull許容の2数値を加算する関数を作成してください。 // safeAdd :: Maybe Number -> Maybe Number -> Maybe Number const safeAdd = undefined;


練習開始!

apの代わりにliftA2を使用して演習_bのsafeAddを書き直してください。 // safeAdd :: Maybe Number -> Maybe Number -> Maybe Number const safeAdd = undefined;


次の演習では以下のヘルパーを考慮します:

js
const localStorage = {  
  player1: { id:1, name: 'Albert' },  
  player2: { id:2, name: 'Theresa' },  
};  
  
// getFromCache :: String -> IO User  
const getFromCache = x => new IO(() => localStorage[x]);  
  
// game :: User -> User -> String  
const game = curry((p1, p2) => `${p1.name} vs ${p2.name}`);

練習開始!

キャッシュからplayer1とplayer2を取得してゲームを開始するIOを作成してください。 // startGame :: IO String const startGame = undefined;