Skip to content

第6章: 実践アプリケーション例

宣言的コーディング

思考の転換が必要です。これからはコンピュータへの命令方法を止め、代わりに望む結果の仕様を記述します。全てを細かく管理するよりも、はるかにストレスの少ない方法だと実感するでしょう。

宣言的とは、命令的ではなく、段階的な指示ではなく式を記述することを意味します。

SQLを例に考えてみましょう。「まずこれを行い、次にあれを行う」という記述は存在しません。データベースから取得したい情報を指定する単一の式があるだけです。処理方法はデータベースエンジンが決定し、アップデートや最適化時もクエリを変更する必要がありません。これは同じ結果を得るための複数の解釈方法が存在するためです。

私を含む多くの人々にとって、宣言的コーディングの概念を最初に理解するのは難しいため、具体的な例で見てみましょう。

js
// imperative
const makes = [];
for (let i = 0; i < cars.length; i += 1) {
  makes.push(cars[i].make);
}

// declarative
const makes = cars.map(car => car.make);

命令型ループでは最初に配列の初期化が必要です。インタプリタは次の処理に進む前にこの文を評価します。次にcarリストを直接イテレートし、カウンターを手動で増加させながら、明示的な反復処理で各部分を見せつけてきます。

mapバージョンは単一の式です。評価順序を必要とせず、イテレーション方法や配列構築方法に自由度があります。処理方法(how)ではなく結果(what)を規定するため、宣言的な性質を持っています。

明確さと簡潔さに加え、map関数は自由に最適化可能であり、アプリケーションコードの変更が不要です。

「命令型ループの方が高速だ」と考える方には、JITコンパイラの最適化方法を学ぶことを推奨します。参考動画:解説動画はこちら

別の例を示します。

js
// imperative
const authenticate = (form) => {
  const user = toUser(form);
  return logIn(user);
};

// declarative
const authenticate = compose(logIn, toUser);

命令型バージョンに問題はありませんが、組み込まれた段階的評価が依然存在します。compose式は単に「認証はtoUserとlogInの合成である」という事実を表明します。これにより実装コードの変更余地を残しつつ、アプリケーションを高水準の仕様として維持できます。

上記例では評価順序が指定されていますが(toUserはlogInより先に呼び出される必要がある)、順序が重要でない多くのシナリオでは宣言的コーディングで容易に表現可能です(詳細は後述)。

評価順序を規定する必要がないため、宣言的コーディングは並列計算に適しています。純粋関数との組み合わせにより、並列・並行システムの実現が容易な点が関数型プログラミングの強みです。

関数型プログラミングによるFlickrアプリ

宣言的で合成可能な方法で実践アプリケーションを構築します。副作用は最小限に抑え、純粋なコードベースから分離します。Flickr画像を取得・表示するブラウザウィジェットを作成します。HTMLスケルトンから開始しましょう:

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Flickr App</title>
  </head>
  <body>
    <main id="js-main" class="main"></main>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.2.0/require.min.js"></script>
    <script src="main.js"></script>
  </body>
</html>

main.jsの基本構造:

js
const CDN = s => `https://cdnjs.cloudflare.com/ajax/libs/${s}`;
const ramda = CDN('ramda/0.21.0/ramda.min');
const jquery = CDN('jquery/3.0.0-rc1/jquery.min');

requirejs.config({ paths: { ramda, jquery } });
requirejs(['jquery', 'ramda'], ($, { compose, curry, map, prop }) => {
  // app goes here
});

lodashの代わりにramdaを使用します。composeやcurryなどが含まれます。requirejsは大げさに思えますが、本書全体で使用するため一貫性を保ちます。

仕様を確認しましょう。アプリは4つの機能を実装します:

  1. 検索語句用URLの構築
  2. Flickr APIの呼び出し
  3. JSON結果をHTML画像に変換
  4. スクリーンへの表示

2つの非純粋なアクション(Flickr APIからのデータ取得とスクリーン表示)を分離します。デバッグ用trace関数も追加:

js
const Impure = {
  getJSON: curry((callback, url) => $.getJSON(url, callback)),
  setHtml: curry((sel, html) => $(sel).html(html)),
  trace: curry((tag, x) => { console.log(tag, x); return x; }),
};

jQueryメソッドをカリー化し引数順を調整。Impure名前空間で副作用のある関数であることを示します。後ほど純粋化します。

Impure.getJSONへ渡すURLを構築:

js
const host = 'api.flickr.com';
const path = '/services/feeds/photos_public.gne';
const query = t => `?tags=${t}&format=json&jsoncallback=?`;
const url = t => `https://${host}${path}${query(t)}`;

モノイドやコンビネータを使ったポイントフリースタイルも可能ですが、可読性を優先します。

API呼び出しと表示処理を行うapp関数:

js
const app = compose(Impure.getJSON(Impure.trace('response')), url);
app('cats');

url関数で生成したURLをtrace付きgetJSONに渡します。アプリ実行時はコンソールにAPIレスポンスが表示されます。

console response

JSONから画像URLを抽出(items内のmediaオブジェクトのmプロパティ):

ramdaのprop関数でネストプロパティ取得。実装例:

js
const prop = curry((property, object) => object[property]);

単純なプロパティアクセス機能。mediaUrls取得に使用:

js
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));

itemsをmapで処理しmediaUrls配列を生成。表示処理と連携:

js
const render = compose(Impure.setHtml('#js-main'), mediaUrls);
const app = compose(Impure.getJSON(render), url);

mediaUrlsをrender関数で表示。生JSONではなく画像URLを画面に出力します。

mediaUrlsをimgタグに変換。大規模アプリではテンプレートエンジンを使用しますが、jQueryで実装:

js
const img = src => $('<img />', { src });

jQueryのhtmlメソッドはタグ配列を受け取れます。mediaUrlsをimgタグに変換しsetHtmlへ渡します。

js
const images = compose(map(img), mediaUrls);
const render = compose(Impure.setHtml('#js-main'), images);
const app = compose(Impure.getJSON(render), url);

完成!

cats grid

完成スクリプト:include

宣言的コーディングによる仕様の美しさに注目してください。各行を等式として捉え、推論とリファクタリングが可能です。

原則に基づくリファクタリング

mapの合成則を活用した最適化可能:

js
// map's composition law
compose(map(f), map(g)) === map(compose(f, g));

この性質を使ってコードを最適化しましょう。

js
// original code
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));
const images = compose(map(img), mediaUrls);

等式推論と純粋性を活かし、mediaUrls呼び出しをインライン化:

js
const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(img), map(mediaUrl), prop('items'));

mapの配置調整後、合成則を適用:

js
/*
compose(map(f), map(g)) === map(compose(f, g));
compose(map(img), map(mediaUrl)) === map(compose(img, mediaUrl));
*/

const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(compose(img, mediaUrl)), prop('items'));

アイテムからimgタグへの変換を1回のループに最適化。可読性向上のため関数を抽出:

js
const mediaUrl = compose(prop('m'), prop('media'));
const mediaToImg = compose(img, mediaUrl);
const images = compose(map(mediaToImg), prop('items'));

まとめ

実践アプリケーションを通じ、数学的フレームワークを使ったコード推論とリファクタリング手法を学びました。エラーハンドリングや純粋性の徹底などは次章で取り組みます。

第7章: Hindley-Milner型システム