関数型プログラミングから予測可能プログラミングへ
by Yuji Yamamoto on July 25, 2015
なんだか興に乗ってきたので、私もポエムめいたものを書いてみようと思います♪
要約: ポエムなんてうんざりな人へ
- 「Haskellが関数型プログラミングの王道だよ」という理解は、誤解のもと。
- 今後このシリーズで言う「関数型プログラミング」とは、 「純粋な関数」を主に使用して行うプログラミングのこと。
- Haskellの本当によいところは 「プログラムの振る舞いを、コードを実行せず、読んだだけで予測できるようにする」という点。
動機: なぜ書くのか
ここでは、私がHaskellをなぜ好むのか、
いわゆる「関数型プログラミング」との関係に触れつつ説明することで、考えを少しずつまとめたい。
一つの記事には収まらないので、複数の記事に分けてシリーズとする。
下心を言えばそのうち本にでもできるとなお面白い。
このシリーズを通して、
私、あるいは他のHaskellerの方々が、同僚や友人のHaskellに対する「誤解」を解き、
Haskellの普及を少しでも前進させることができたらよいと思う。
「Haskellは関数型プログラミング言語の王道である」、
「モナドなどHaskellが提供する機能が関数型プログラミングの真髄である」という理解は、
もちろん「関数型プログラミング」の定義によるものではあるが、
Haskellを理解する上で必ずしも適切ではないし、混乱のもとである。
ここ3年ほど趣味の範囲ながらHaskellを学ぶことで、私はそのことに気づき、
その「関数型プログラミング」らしい部分とそうでない部分との、絶妙な関係に、深く驚いた。
そこでここでは私が最も好きなHaskellの特性を、
「予測可能プログラミング」という言葉に象徴させることで、
そうした関係とその素晴らしさをわかりやすく整理したい。
手始めに、この記事では今後使う重要なキーワードの定義を述べておこう。
ゆっくり書くので後から変えることがあるかもしれないが、
その時は必ずみなさんには伝えるのでご容赦願いたい。
定義: ここで言う、「関数型プログラミング」
今更改めて言うことでもないが、定義を明確にしないまま主張したり議論することは、不毛のもとである。
というわけで予めこのシリーズで使用する「関数型プログラミング」の定義をここで示そう。
- ここで言う「関数型プログラミング」とは次に定義する「純粋な関数」を主に使用してプログラムのソースコードを組み立てるプログラミングの手法のことである。
- ここで言う「純粋な関数」とは、
- 与えられた入力(引数)に対応する出力(返り値)が一意に定まる(引数を決めれば返り値が必ず一つに決まる)、
- 「出力(返り値)を返す」以外にプログラムの状態に影響を及ぼさない、
ここで肝要なことは、使用する関数が「純粋な関数」であることであり、
ソースコードの全てが「純粋な関数」で書かれていることや、
ある言語の全てのAPIが「純粋な関数」であること、
特定の関数が「純粋な関数」であるかどうかは、必ずしも重要でない。
例えば、次のRubyで書かれた関数は中身は手続き型スタイルで書かれているが、
関数全体としてみると、純粋な関数となっている。
def sum numbers
= 0 # 最終的に結果として返す変数を初期化して...
result .each do|n|
numbers+= n # 書き変えていって...
result end
# 結果として返す
result end
上記は、result
の状態を書き変えることで結果を組み立てる、
典型的な手続き型スタイルの関数だが、状態の書き換えは関数のスコープに閉じているため、
同じ引数(numbers
)に対しては必ず同じ結果を返すし、
プログラム内のほかの変数など、外の状態に影響を与えることはない。
したがって、この関数を使うこと自体は関数型プログラミングスタイルに全く反しない。
これはちょうど、次の、HaskellのSTRef
を使った関数が純粋な関数であることと全く同じことである。
-- Haskellが分からない場合はとりあえず読み飛ばそう!ここではそんなに重要じゃない!
sum :: [Int] -> Int
sum numbers = runST $ do
<- newSTRef 0 -- 最終的に結果として返す変数を初期化して...
result $ \n -> do
forM_ numbers +n) -- 書き変えていって...
modifySTRef' result (-- 結果として返す readSTRef result
また、ここでの議論に限ったことではないが、
前述の通り「関数型プログラミング」はあくまでもプログラミングの手法、
言い換えるとプログラミングスタイルであり、
特定の言語に存在する機能やそのパラダイムを指すものではないことを、加えて強調したい。
「関数型プログラミング言語」というのはせいぜい
「関数型プログラミングを強力にサポートする言語」
程度の意味しかないと捉えて頂いて差し支えない。
そして前述のように定義された「関数型プログラミング」を言語として強力にサポートすることが、
(とりわけHaskellにおいて)非常に素晴らしい意味を持つ、
ということを私は今後の記事で伝えていきたい。
定義: ここで言う、「予測可能プログラミング」
コードを読んでその動作を予測できることに、誰が不満を持つだろうか。
この記事での造語、「予測可能プログラミング」とはまさしくそうした、
「人、あるいはコンピューターがプログラムのソースコードを読んだだけで、
実行することなくその振る舞いを予測できるよう、ソースコードを書くこと」なのである。
Haskellやその他の言語などとの関係
今後私が最も主張していきたいことは、 現在一般にHaskellが優れているとされている(と、私が思っている)機能の多くは、 この「予測可能プログラミング」を強く支援するところにあるのではないか、ということだ。
もちろん、これもHaskellをはじめ特定の言語だからできる、というものでもない。
RubyであれCであれJavaScriptであれ、人間が工夫してソースコードを書くことで、
読んだだけで振る舞いを予測できるソースコードは書ける。
というか、それができない言語などまともなプログラミング言語ではないだろう。
このシリーズではそれを、各言語の構文や機能がどうサポートするのか、Haskellを中心に解説する。
ただし、
Haskellがその「予測可能プログラミング」をする上で最高の言語だと主張するつもりは毛頭ない。
Haskellである程度プログラムを書いたことがある方にとって当然だとは思うが、
Haskellにも「予測可能プログラミング」を、支援するどころか阻むような仕様はある。
最たる例をあげるなら、恐らく遅延評価1であろう。
ここでは詳細を割愛するが、遅延評価は諸刃の剣であり、
思わぬ場面でスペースリークという、メモリーリークのような現象を起こす原因となる。
「思わぬ場面で」プログラムに好ましくない振る舞いをもたらすということは、
「予測可能プログラミング」に真っ向から反する。
Haskellは古くて新しい言語なので、
最初から「予測可能プログラミング」のために作られたわけではないのだ。
「関数型プログラミング」との関係
私が言う、「予測可能プログラミング」にとっての「関数型プログラミング」は、 「予測可能プログラミング」を支援する一方法論に過ぎない。詳細は次回以降述べる。
また、関数型プログラミングのよい点すべてが、
「予測可能プログラミング」を支援できることではないことも、ここで明確にしておこう。
より小さなパーツから組み合わせてプログラムを組み立てられる、という点は、
どちらかというと、モジュラリティや再利用性に関するよさであろう。
残念ながらこのシリーズでそれに触れるつもりはないが、
改めてHaskellやその他のいわゆる「関数型プログラミング言語」を学ぶ時の楽しみにしていただきたい。
おことわり
何か間違いや質問、気になる点があった場合、 igrep@n.email.ne.jp にメールを送るか、このブログを管理しているGitHubのリポジトリに Issueを送っていただきたい。
それではこれから、少しずつになるが、 「関数型プログラミングから予測可能プログラミングへ」シリーズの続きを書いていくので、 これからも読んでいただけるとありがたい。
Haskellの言語規格上、 実際には非正格評価と言った方が適切ではあるが、 実際のところGHCはじめ多くの処理系は遅延評価なので、ここでは遅延評価ということにしておく。
なお、各処理系と規格の仕様の違いについてついでに触れておくと、 特に断りがある場合を除いて、ここではGHCの仕様についてのみ説明する、ということにする。 単純に私自信がGHC以外の処理系のことをよく知らないのと、 GHCが十分に普及した、人気の高い処理系であるためである。 ご容赦していただきたい。↩︎