純粋関数で純粋な幸せを手に入れよう - most adequate guide Chapter 3のテキトーな訳
このページの趣旨
下記ページの、かなり「大雑把」に、「私が興味を持ったところだけ」を「雰囲気」で翻訳したものです。
mostly-adequate.gitbooks.io Chapter 03: Pure Happiness with Pure Functions
原文をかなり崩して書いている(原文にない単語、文章を追加したりしている箇所もある)ので、翻訳とすら、呼べないものかもしれません。
違和感を感じたら、原文を参照することをおすすめします。
Chapter3: 純粋関数で純粋な幸せを手に入れよう
純粋さを取り戻そう
まず最初に学ばないといけないのは、純粋関数の考え方です。
純粋関数とは、同じ入力からは、必ず、同じ出力が得られる関数で、副作用のないものをいいます
たとえば、slice
とsplice
を考えてみましょう。この2つの関数は、まったく同じことをする関数です。ただ、その処理の方法は、まったく違います。
slice
は、毎回、同じ入力から同じ出力が得られるので純粋関数です。一方で、splice
は、元配列をグチャグチャにして、違うものにしてしまいます。これが副作用です。
const xs = [1,2,3,4,5];
// 純粋関数
xs.slice(0,3); // [1,2,3]
xs.slice(0,3); // [1,2,3]
xs.slice(0,3); // [1,2,3]
// 純粋関数でない
xs.splice(0,3); // [1,2,3]
xs.splice(0,3); // [4,5]
xs.splice(0,3); // []
関数型プログラミングでは、splice
のような元データを変更するような、扱いにくい関数は好みません。私たちは、信頼できる関数を使いたいので、このようなものは使いません。
信頼できる関数というのは、毎回、同じ結果を返す関数であって、splice
のような、後に混乱を残すような関数ではないのです。
それでは、別の例を見てみましょう。
// 純粋関数ではない
let minimum = 21;
const checkAge = age => age >= minimum;
// 純粋関数
const checkAge = (age) => {
const minimum = 21;
return age >= minimum;
};
前者の「純粋関数ではない関数」を見てみましょう。
checkAge
は、変更可能な変数であるminimum
の内容に応じて、関数の出力が変わります。言い換えると、この関数は、システムの状態に依存しているのです。
これは、残念なことです。というのは、関数の実行結果を考えるときに、外部環境を考慮する必要が出てくるため、考えるべきことが増えてしまうからです。
この例だけでは、それほど重大なことには感じられないかもしれませんが、状態に依存することは、システムを複雑化する一番大きな要因なのです(http://curtclifton.net/papers/MoseleyMarks06a.pdf)。
このcheckAge
は、入力以外の要因に依存して、別の結果を返します。これは、関数の純粋性を失うだけではなく、私たちが、プログラムの挙動を考えるときにも、苦労することになります。
一方で、後者の「純粋関数」は、自己完結しています。変数minimum
も不変にすることができます。状態が絶対に変わらないことを保証できるため、純粋性を保つことができるのです。
これをオブジェクトで行いたいときには、次のようにfreezeする必要があります。
const immutableState = Object.freeze({ minimum: 21 });
副作用には、何が含まれるのか?
それでは、さらに副作用がどういうものかを見ていきましょう。
純粋関数の定義の中で悪いものとして言及されている副作用とはどういうものなのでしょうか?これからは、計算結果(返り値)として、計算されたもの以外に発生するすべてのものを「作用」と書いていきます。
作用そのものは悪いものではありませんし、これから後のChapterの至るところで作用を使っていきます。
問題があるのは、その作用に付随する部分です。たとえば、水自体は何も問題ありませんが、よどんだところには虫がわきます。副作用は、それと同じように、あなたのプログラムのバグの発生源となるのです。
副作用は、システムの状態を変えたり、外部との間に観測できる入出力を生じるもので、返り値を計算する途中で発生するものです。
副作用には、次のようなものが含まれます(それ以外のものもあります)。
- ファイルの変更
- データベースへのデータの挿入
- HTTP呼び出し
- (オブジェクトの)変更
- 画面出力、ログ出力
- ユーザからの入力の受取
- DOMからの情報取得
- システムの状態へのアクセス
もちろん、副作用の例は、他にもたくさんあります。functionの外に対する、あらゆる相互作用は副作用です。
この一覧を見ると、本当に、このような作用なしに、実用的なプログラムが組めるのか疑問に感じるかもしれません。でも、関数型言語の世界では、副作用は、プログラムが正しく動かない主な原因であると考えています。
だからといって、これらの副作用を使うことを禁止しているわけではありません。むしろ、これらの副作用を、適切に統制し、制御された方法で扱いたいのです。その方法は、後の章で、関手(functors)や、モナド(monads)が出てきたときに学びます。とりあえず、今は、これらの面倒な機能は扱わないことにしましょう。
副作用があると、関数は、純粋関数になりません。そして、純粋関数は、(定義からわかるように)常に、同じ入力に対して、同じ出力を返す必要があります。逆に、関数が、関数外部の事象を扱っていたら、絶対に、この純粋性を保証することはできないのです。
これから、なぜ、同じ入力に対して同じ出力が得られることが必要なのか、もう少し詳細に見ていきましょう。まずは、(中学で習うレベルの)数学の話を見ていきましょう。
関数とは何か
mathisfun.comによると、関数とは次のようなものです。
関数とは、複数の値の間の、特別な関係です。入力された値に応じて、ただ1つの出力する値を返します。
言い換えると、関数は単なる、「2つの値(入力と出力)の間の関係」です。それぞれの入力に対して、ただ1つの出力を持ちますが、その出力は、入力ごとに違う値を取る必要はありません。たとえば、次の図で表される「xとyの関係」は、関数の定義にあてはまります。
https://www.mathsisfun.com/sets/function.html
一方で、下記の図の「xとyの関係」は、関数ではありません。なぜなら、「5」という入力に対して、複数の出力があるからです。
https://www.mathsisfun.com/sets/function.html
関数は、複数の「(入力、出力)の組」で表現することができます。たとえば、[(1,2), (3,6), (5,10)]は、入力を2倍する関数に見えます)。
あるいは、表の形で表すこともできます。
入力 | 出力 |
---|---|
1 | 2 |
2 | 4 |
3 | 6 |
あるいは、xを入力、yを出力してグラフで表すこともできます。
入力から出力が決まりさえすれば、関数を詳細に実装する必要もありません。関数は、単に、入力と出力を対応づけるものであればなんでも構いません。ですから、関数を使わずに、単純なオブジェクトリテラルで書き下しても構いません。その場合には、()
の代わりに[]
を使うことになります。
const toLowerCase = {
A: 'a',
B: 'b',
C: 'c',
D: 'd',
E: 'e',
F: 'f',
};
toLowerCase['C']; // 'c'
const isPrime = {
1: false,
2: true,
3: true,
4: false,
5: true,
6: false,
};
isPrime[3]; // true
もちろん、個別に対応を書き下すよりも、計算で、結果を導出したい、と思うときもあるでしょう。でも、これは、関数について違った考え方ができることを表しています。
ひょっとしたら、あなたは、複数の引数を取る関数はどうなるのだろう?と考えているかもしれません。たしかに、数学的に考えるときには、少し表しにくい部分もあります。
とりあえず、今の段階では、「複数の引数」を配列としてまとめたarguments
オブジェクトを入力として使う、と考えてみてください。後で、カリー化を学ぶと、数学的な関数の定義を、プログラミングの関数に、直接適用する方法がわかるようになります。
ここまでで、重要なことがわかりました。純粋関数というのは、数学的な関数だということです。そして、これが、関数型言語のすべてなのです。
この単純なパーツを使ってプログラミングをすることで、大きな利益を得ることができます。これから、なぜ、純粋性を保つために多大な努力をする必要があるのか、その理由を見ていきましょう。
純粋性が重要な場面
キャッシュ可能
まず最初に、純粋関数は、常に、入力に応じて、出力をキャッシュすることができます。出力をキャッシュするために、典型的には「メモ化」という技術が使われます。
const squareNumber = memoize(x => x * x);
squareNumber(4); // 16
squareNumber(4); // 16, 入力「4」に対するキャッシュを返します
squareNumber(5); // 25
squareNumber(5); // 25, 入力「5」に対するキャッシュを返します
次のソースコードは、キャッシュの簡略化した実装です。もちろん、より堅牢に実装されたライブラリも、たくさんあります。
const memoize = (f) => {
const cache = {};
return (...args) => {
const argStr = JSON.stringify(args);
cache[argStr] = cache[argStr] || f(...args);
return cache[argStr];
};
};
注意すべき点として、評価(関数の実行)を後回しにすることで、非純粋関数を純粋関数に変換することができる、ということです。
const pureHttpCall = memoize((url, params) => () => $.getJSON(url, params));
ここで興味深いのは、実際にHTTP呼び出しを行っていないところです。その代わりに、「呼び出したときに、HTTP呼び出しが実行される」functionを返却しています。
このfunction(pureHttpCall)は純粋関数です。なぜなら、同じ入力に対して、同じ出力が得られるからです。url
とparams
を与えると、特定のHTTP呼び出しを行う関数が得られます。
もちろん、先ほど作ったmemoize
関数も、問題なく動きます。とはいえ、HTTP呼び出しの結果をキャッシュするわけではなく、生成されるfunctionをキャッシュすることに注意しましょう。
この方法は、今のところ、それほど有用とはいえません。でも、後で、この方法を有用に使う「手法」を学ぶことになります。
ここで学んでほしいのは、どんな関数であっても、たとえその関数が破壊的なものに感じたとしても、キャッシュできるということです。
可搬性がある、自己文書化されている
純粋関数は、完全に、自分自身で完結しています。関数が必要とするすべてのものが、常に揃った状態になっています。
これがどのような効果を持つのか、少し考えてみてください。
まず、関数の依存関係が明示されているため、読みやすく理解しやすいです。見えないところで、何か怪しげなことが行われているということはありません。
// 非純粋関数
const signUp = (attrs) => {
const user = saveUser(attrs);
welcomeUser(user);
};
// 純粋関数
const signUp = (Db, Email, attrs) => () => {
const user = saveUser(Db, attrs);
welcomeUser(Email, user);
};
この例でわかるように、純粋関数は、何に依存しているかをすべて書く必要があります。その結果、純粋関数の定義を見れば、何が起きるのかが正確にわかります。関数の引数を見るだけで、少なくとも、その関数ではDb
、Email
とattrs
を使うということがわかります。
これから、私たちは、(単に)評価を遅延する方法ではない方法で、このような純粋関数をどう作るかを学んでいきます。明確にしておきたいのは、純粋な関数は、誰が何を知っているか次第で動きが変わる非純粋関数よりも有益であるということです。
それとは別に意識してほしいのは、純粋関数では、依存性を注入すること、あるいは、引数として依存する対象を渡すことを強制されている、ということです。
これにより、この関数を使ったアプリケーションは、より柔軟なものになります。なぜなら、私たちは、データベースやメールクライアントなどを、パラメータ化したからです(面倒に感じるかもしれませんが、後で、その面倒さを軽減する方法を紹介するので、安心してください)。
別のデータベースを使うことにした場合であっても、この関数を、その別のデータベースを引数にして呼び出せばいいだけです。別のアプリケーションを作る場合に、この信頼できる関数を再利用したいときでも、単に、Db
とEmail
の部分を、そのとき使っているものに置き換えて、この関数を呼び出せばいいのです。
JavaScriptの設定では、ポータビリティがある(可搬性がある)ということは、関数をシリアライズしてソケット通信で関数を送付できるということを意味します。ということは、Web Workersなどでも、コードを動かせるということです。ポータビリティがあるということは、とても強力な特性なのです。
命令型プログラミングで作られた「典型的な」メソッドやプロシージャは、その状態、依存関係、利用可能な結果などの環境に大きく依存してしまいます。それに対して、純粋関数は、どこでも好きなところで動かすことができるのです。
あなたが最後にメソッドを、別のプログラムにコピーしたのはいつでしょうか?私は、Erlangを作ったJoe Armstrongの次の言葉を、よくお伝えしています。「オブジェクト指向言語の問題は、暗黙の前提となっている環境も一緒に持ち歩かないといけないことです。単にバナナが欲しいだけなのに、バナナを持っているゴリラだったり、ときには、ジャングル全体を持ち歩かないといけないのです。」
テストしやすい
次に、純粋関数は、テストを書くのが、比較的簡単だということに気付くでしょう。実際の支払ゲートウェイや、外部の世界の状態を設定し、結果を想定する必要はありません。単に、関数の入力と想定される出力を与えれば良いのです。
実際、関数型言語のコミュニティでは、新しいテストツールを生み出しています。それは、関数に、自動生成した膨大な数の入力を与えて、出力が満たすべき性質を想定するようなものです(Property-Based-Testing)。それは、この本の範囲からははずれますが、ぜひ、そのツール「Quickcheck」を探して、試してみることを強くおすすめします。このツールは、純粋関数の環境に最適化されたテストツールです。
合理的である
多くの人が、純粋関数を使う、一番大きなメリットは参照等価性を確保できることだと考えています。プログラムの一部が「参照等価的」というのは、挙動を変えずにプログラムの一部の「コード」を「そのコードを評価した値」に変更できることをいいます。
純粋関数は副作用を持たないので、プログラムに対して影響を与えるためには、出力した値を使うしかありません。さらに、純粋関数が出力する値は、入力された値から信頼性をもって計算することができます。ですから、純粋関数は、常に参照等価性を持つことになります。実際の例を見てみましょう。
const { Map } = require('immutable');
// Aliases: p = player, a = attacker, t = target
const jobe = Map({ name: 'Jobe', hp: 20, team: 'red' });
const michael = Map({ name: 'Michael', hp: 20, team: 'green' });
const decrementHP = p => p.set('hp', p.get('hp') - 1);
const isSameTeam = (p1, p2) => p1.get('team') === p2.get('team');
const punch = (a, t) => (isSameTeam(a, t) ? t : decrementHP(t));
punch(jobe, michael); // Map({name:'Michael', hp:19, team: 'green'})
decrementHP
、isSamteTeam
とpunch
は、すべて純粋関数なので、参照等価性を持ちます。そのような場合には、「等式推論」と呼ばれる技術を使うことができます。
これは、ある2つの値が等しいことが分かっている場合に、その1つを別の1つに置き換えることで、コードの動きを推論する方法です。
これは、プログラム的な評価のクセを考慮せずに、コードを手動で評価するようなものです。参照等価性を使って、少し、このコードを解析してみましょう。
最初に、function isSamteTeam
を、インライン展開してみましょう。
const punch = (a, t) => (a.get('team') === t.get('team') ? t : decrementHP(t));
今回使っているデータは変更できない(immutable)ので、チームは、実際の値に置き換えることができます。
const punch = (a, t) => ('red' === 'green' ? t : decrementHP(t));
この等式の評価の結果はfalseなので、条件分岐を取り除くことができます。
const punch = (a, t) => decrementHP(t);
さらに、function decrementHP
をインライン展開すると、punchは、hpから1を引く処理に置き換わります。
const punch = (a, t) => t.set('hp', t.get('hp') - 1);
一般的に、コードについて、このような推論ができるということは、コードのリファクタリングをしたり、コードの理解をするために、とても効果的です。
実際、このテクニックを使って、Chapter1で、カモメの群れのリファクタリングを行いました。そこでは「等式推論」を使って、足し算・掛け算の性質に結びつけたのでした。たしかに、私たちは、この本を通じて、このテクニックを使います。
並列実行
最後に、とどめの一撃として、純粋関数は並列に実行することができます。なぜなら、並列関数は、共有メモリにアクセスする必要はなく、また、(定義からわかるように)副作用を原因とする競合を起こすこともありません。
サーバサイドJavaScript環境では複数スレッドを使ったプログラムを動かせますし、同じように、ブラウザ環境でもWeb Workerで複数スレッドを使ったプログラムを動かせます。ただ、現時点では、非純粋関数を処理するときの複雑さを回避するために、複数のスレッドを使ったプログラムはあまり使われていないようです。
まとめ
この章では、純粋関数とはどういうものか、そして、関数型言語のプログラマーが、なぜ、純粋関数を非常に重要だと考えているかを見てきました。この観点からは、私たちは、すべての関数を純粋関数として書きたいと切望しています。そのためには、いくつか追加のツールが必要になります。現時点では、純粋なコードから、非純粋関数を分離して扱うことにしましょう。
純粋関数を使って、プログラムを書くことは、追加のツールがないと、やや面倒です。たとえば、状態を使ってはいけないため、当然、すべての箇所に、引数を通じてデータをやり取りしないといけません。どうやって、こんな面倒すぎるプログラムを組めばいいのでしょうか?
そこで、次の章で、カリー化という新しいツールを手に入れましょう。