関数型プログラミングを応用してプログラムを書く - most adequate guide Chapter 6のテキトーな訳
このページの趣旨
下記ページの、かなり「大雑把」に、「私が興味を持ったところだけ」を「雰囲気」で翻訳したものです。
mostly-adequate.gitbooks.io Chapter 06: Example Application
原文をかなり崩して書いている(原文にない単語、文章を追加したりしている箇所もある)ので、翻訳とすら、呼べないものかもしれません。
違和感を感じたら、原文を参照することをおすすめします。
Chapter6: 関数型プログラミングを応用してプログラムを書く
宣言的コーディング
これからは、プログラムを組むときの考え方を変えましょう。
コンピュータにどのような処理をしてほしいかを書くのは止めて、これからは、どのような結果が欲しいかを書くようにしていきましょう。常に、細かい作業の仕方を管理しようとするのに比べて、はるかにストレスが少なくてすむようになるはずです。
「宣言的」というのは、式を書くということです。それに対して、「手続き的」というのは、1つ1つの指示を書くことを表します。
たとえば、SQLを考えてみてください。
そこには「最初にこれをして、次にこれをする」というような記述はありません。そこには、1つの式があって、データベースから何を取ってきてほしいかが書かれています。どうやって、その作業をするのかを、あなたが決めることができません。SQLが決めるのです。
データベースがアップグレードされて、SQLの解析エンジンが最適化されたときでも、クエリを変える必要はありません。なぜなら、仕様を満たし、同じ結果を得られるような方法は何通りもあるからです。
ある人にとっては(私もそうなのですが)、宣言的なプログラムを見ても、ぱっと何をしているかが理解するのが大変です。
それを実際に感じてもらうために、いくつか例を見てみましょう。
// 手続き的
const makes = [];
for (let i = 0; i < cars.length; i += 1) {
makes.push(cars[i].make);
}
// 宣言的
const makes = cars.map(car => car.make);
手続き的にループを書くときには、最初に配列を初期化しておく必要があります。インタープリタは、必ず、この文を解釈してから、実際の処理に入ることになります。次に、車のリストを反復する処理を直接書き、手動でカウンターを増やし、こまごまとした醜い反復処理の内容を明示的に書いています。
一方で、宣言的な書き方では、1つの式があるだけです。評価の順番について指示する必要はありません。map関数が、どのように反復処理をして、返り値となる配列を組み立てるかについての自由度は高いです。どうすべきかではなく、何をすべきかを指示しているのです。
この書き方は、明確で簡潔であることに加えて、仮にmap関数の実装が最適化されたとしても、プログラムを書き替える必要はありません。
それでも「その話はわかった。けれど、手続き的にループを書いたほうが処理速度が速い」と考えているようであれば、JITがあなたのコードをどのように最適化しているかを勉強してみることをおすすめします。たとえば、この素晴らしいビデオを見てみるといいでしょう。
また、別の例も見てみましょう。
// 手続き的
const authenticate = (form) => {
const user = toUser(form);
return logIn(user);
};
// 宣言的
const authenticate = compose(logIn, toUser);
手続き的なコードは、完全に間違えている、というわけではありませんが、1つ1つ評価をする、という指示がコード中に残っています。
一方で、compose
を使ったコードでは、事実関係を記述しているだけです。書かれているのは、認証ロジックはtoUser
とlogin
を合成したもの、ということだけです。
繰り返しになりますが、こう書くことで、実装されるコードが最適化される余地が残り、その結果、コードは、より高度なものになるのです。
先ほどの手続き的なコードでは、評価の順番は、決められています(toUser
が最初で、その後にlogin
が呼ばれます)。でも、評価の順番が重要でないケースも多々あります。そして、宣言的なコードであれば、評価の順番が重要でない旨を簡単に指定することができます(詳細は後述)。
宣言的なコードは、評価の順番を指定する必要がないため、並列計算に適しています。この「宣言的なコードが書ける」ことと「純粋関数を使う」ということが、将来的な並列化を視野に入れたときに、関数型プログラミングが良い選択肢になる理由です。並列、同時実行するシステムを作るために、特別なことをする必要はありません。
関数型プログラミングでFlickr APIを使う
それでは、composeを使って宣言的に、サンプルアプリケーションを作ってみようと思います。
今のところは、ちょっとズルをして、副作用を使います。ただ、副作用を使うのは最小限に、かつ、純粋なコードと分離して配置します。
今回は、flickr(というアプリ)の画像を取り込んで表示するブラウザのウィジットを作ってみましょう。
それでは、アプリの枠組みを作ってみましょう。下記が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>
そして、これが、Java Scriptのひな型です。
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 }) => {
// ここにコードを書いていきます。
});
関数型プログラミングのライブラリとして、lodashなどではなく、ramdaを使います。ramdaにはcompose
、curry
などが含まれています。
また、requirejsを使ってライブラリを読み込みます。やりすぎな気もしますが、この本では一貫して使っていきます。
それでは、本題に戻って、仕様を説明しましょう。私たちが作るアプリでは、次の4つのことを行います。
- 検索したい単語に応じてURLを組み立てる
- flickrにAPI呼び出しを行う
- その結果のJSONをHTMLの画像に変形する
- その結果を画面に表示する
この中には、2つ、非純粋な操作が含まれています。それが何かわかるでしょうか?それは、flickr APIからデータを取得するところと、その結果を画面に表示するところです。
そこで、最初に、これらの部分を実装して隔離できるようにしておきましょう。また、trace
関数を追加して、デバッグをしやすくなるようにしましょう。
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のメソッドを単純にラップしています。そのときに、カリー化するとともに、引数の順番をより使いやすくなるように入れ替えています。そして、Inpure
という名前でまとめることで、これらが危険な関数であるということをわかるようにしています。なお、後の例では、これらの2つの関数を純粋関数に修正します。
次に、Inpure.getJSON
関数に渡すためのURLを組み立てます。
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)}`;
実際には、おしゃれで、かつ、かなり複雑ですが、モノイド(後で学びます)やコンビネータを使って、pointfreeにurl
を作る方法もあります。ただ、今回は、読みやすさを優先して、普通のpointfulな方法でurlを組み立てます。
それでは、APIを呼び出して、画面にデータを表示するapp
関数を書いてみましょう。
const app = compose(Impure.getJSON(Impure.trace('response')), url);
app('cats');
このapp
は、まずurl
関数を呼び出し、その結果の文字列をgetJSON
関数に渡します。なお、そのgetJSON
関数は、1つ目の引数にtrace
関数が部分適用されています。実際に実行してみると、APIからのレスポンスが画面に表示されます。
このJSONデータから、画像表示用のデータを作っていこうと思います。どうやら、画像のURLはitems
の中にあり、さらに、それぞれのmedia
のm
プロパティから取得できるようです。
このネストされたプロパティを取得するために、ramdaで定義されている、一般的なgetter関数であるprops
関数を使うことができます。次のprops
を自力実装したバージョンを見ると、何が起きるのかわかりやすいでしょう。
const prop = curry((property, object) => object[property]);
かなり退屈な定義ですが、行っていることは、指定したオブジェクトに、[]
を使って、プロパティにアクセスしているだけです。
では、この関数を使って、mediaUrls
を取得しましょう。
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));
items
部分を取得したら、それぞれのitemのurlを取得するためにmap
する必要があることに注意してください。これを実行すると、mdeiaUrls
に配列が入ります。
これをアプリに組み込んで、取得した結果を画面に表示させましょう。
const render = compose(Impure.setHtml('#js-main'), mediaUrls);
const app = compose(Impure.getJSON(render), url);
ここまででやったことは、mediaUrls
を呼び出して、その結果をHTMLの<main>
の部分に配置するように、関数を結合しただけです。そして、trace
を呼び出していたところを、render
に置き換えたことで、JSONデータそのままではなく、適切な内容を表示するようにしました。これで、mediaUrls
の内容が、そのまま本文内に表示されます。
最後に、このmediaUrls
を、実際の画像に変換しましょう。もっと大きなアプリケーションでは、HandlebarsやReactなどのテンプレート/DOMライブラリを使ったほうがいいでしょう。
ただ、今回作るアプリでは、imgタグがあればいいだけなので、jQueryを使います。
const img = src => $('<img />', { src });
jQueryのhtml
メソッドには、HTMLタグが入った配列を渡すこともできます。ですから、「mediaUrls」を「imgタグ」に置き換えて、setHTML
メソッドに送れば、アプリは問題なく動きます。
const images = compose(map(img), mediaUrls);
const render = compose(Impure.setHtml('#js-main'), images);
const app = compose(Impure.getJSON(render), url);
これで、完成です!
完成版のスクリプトは次のとおりです。
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 } });
require(['jquery', 'ramda'], ($, { compose, curry, map, prop }) => {
// -- Utils ------------
const Impure = {
trace: curry((tag, x) => { console.log(tag, x); return x; }),
getJSON: curry((callback, url) => $.getJSON(url, callback)),
setHtml: curry((sel, html) => $(sel).html(html)),
};
// -- Pure -------------
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)}`;
const img = src => $('<img />', { src });
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));
const images = compose(map(img), mediaUrls);
// -- Impure ------------
const render = compose(Impure.setHtml('#js-main'), images);
const app = compose(Impure.getJSON(render), url);
app('cats');
});
できあがったコードを見てみましょう。
仕様がどうあるべきかが美しく宣言され、どうやってそうなるかは書かれていません。
各行は、どういう特性を持つべきかを表す等式が書かれています。
そして、これらの特性を使って、どういうアプリケーションかを推測したり、リファクタリングをすることができます。
法則に基づいたリファクタリング
先ほどのプログラムは、さらに最適化ができます。というのは、先ほどのプログラムでは、mapを使って各itemを画像のURLに変え、さらにもう一度mapを使って画像のURLをimgタグに変えているからです。
mapと結合(composition)の間には、次のような法則があります。
// mapの結合法則
compose(map(f), map(g)) === map(compose(f, g));
この法則を使って、先ほどのコードを最適化しましょう。これが、法則に基づいたリファクタリングです。
// 元々のコード
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));
const images = compose(map(img), mediaUrls);
まず、mapをしている箇所を1列に並べましょう。具体的には、images
を処理するときのmediaURLs
を呼び出す処理をインライン化します。
等式推論と純粋関数のおかげで、このような処理ができるのです。
const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(img), map(mediaUrl), prop('items'));
map
が1列に並んだので、「mapの結合法則」を適用することができます。
/*
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'));
これで、このアプリは、1回のループ処理だけで、各itemをimgタグに変換できるようになりました。さらに、少し読みやすくするために、一部分の機能を抽出しましょう。
const mediaUrl = compose(prop('m'), prop('media'));
const mediaToImg = compose(img, mediaUrl);const images = compose(map(mediaToImg), prop('items'));
まとめ
このChapterでは、今まで学んできた技術を使って、小さいながらも実際に動くアプリを作りました。そして、数学的なフレームワークを使って、コードを読み、リファクタリングを行いました。
とはいえ、いくつかの疑問も残ります。
- エラーハンドリングや、条件分岐はどのように書けばいいのでしょうか?
- 今回は名前空間を分離して破壊的な関数を使いましたが、それの代わりに純粋関数だけでアプリを作るには、どうすればいいのでしょうか?
- アプリを安全に、表現力豊かなものにするためには、どうすればいいのでしょうか?
これらの疑問には、次のChapter以降で取り組んでいきます。