##なぜ関数プログラミングは重要か
なぜ関数プログラミングは重要かを読んでみた。
本文の結論として、次のようにある。
生産性の向上を目的とした言語はモジュラプログラミングをよくよくサポート しなければならない。しかし、新しいスコープ規則や分割コンパイルの機能で は不十分である。モジュール性はモジュール以上の意味がある。問題を部分に分解する能力は解を貼り合せる能力に直接に依存している。モジュラプログラ ミングを支援するには、言語は良い糊を用意しなければならない。関数型プロ グラミング言語は二つの新しいタイプの糊を供給する。すなわち、高階関数と 遅延評価である。これらの糊を用いて新しいわくわくするような方法でプログ ラムを簡単にモジュール化できる。(中略)もしプログラムの部分のどこかがごちゃごちゃで複雑なも のであるなら、プログラマはそれをモジュール化し、その部品を汎用化しなけ ればならない。これを実現するためのツールとして高階関数と遅延評価が使え ることを期待するはずだ。(後略)
プログラマ目線で要点を抜き出してみると、
- 生産性の向上を目的するならモジュール化が必要だ
- 問題を部分に分解することは解を貼り合わせることに依存している
- 高階関数と遅延評価が貼り合わせに有効なツールとなる
- ごちゃごちゃなプログラムの部分を見つけたらモジュール化しなくてはならない
といったところだろうか。
総論としては理解でき、サンプルコードも読めるのだが、いざ自分で実践しようとすると応用が利かない。特に問題を部分に分解するという点で、「で、この場合はどう分解するのが正しいの?」がわからない。
ということで、 問題を部分に分解してから解を貼り合わせることでモジュール化するという手法について、言葉を少し調べてみることで、自分の指針となる考え方がまとまれば良いと思う。
モジュール化
プログラムをモジュール化すると、大きな恩恵を得ることができる。これは本文に記述されている内容がわかりやすく、そういうことなのだろう。
モジュール化設計は大きな生産性向上をもたらす。まず第一に小さなモジュールは素速くかつ早期にコード 化できる。第二に汎用のモジュールは再利用可能で、これが後のプログラムを 速く開発することを可能にする。第三にプログラムのモジュールは独立に試験 することが可能で、これがデバッグ時間の節減に役立つ。
また、モジュールという言葉の定義を見てみると
モジュール(Module)とは、工学などにおける設計上の概念で、システムを構成する要素となるもの。いくつかの部品的機能を集め、まとまりのある機能を持った部品のこと。モジュールに従っているものをモジュラー (modular)という。
入出力を絞り込み、標準化することで、システム開発を「すり合わせ」から「モジュールの組合わせ」にすることができる。
まとまりのある機能を持った部品とある。明示的にこれこれ、と決まっているわけではないが、分解するための方法論がある。大きな機能を、何らかの粒度で分割するとモジュールと呼ばれるようになっていくのだろう。またこの「何らかの粒度」が規定されているなら、その規定な有効な組織内で、モジュール化の仕組みが標準化されることになる。
このような形で、ある機能をモジュールに分解することをモジュール化と呼ぶのが自然だと思う。もう少し詳しく言うと、
sumの計算は一般的な再帰パターンと箱のなかの部分とを貼り合せることでモジュール化できることを意味している。
という用例から想像するに、ある操作を、(汎用的な部品を含めた)複数の部品の組み合わせで表現する ということを本文ではモジュール化と呼んでいるように思う。
問題を部分に分解して解を貼り合わせる
関数プログラミング関連の本を読んでいると、問題を部分に分解するという表現がよく目につく。どこかで見たことがあると思っていたが、いかにして問題をとくかという本にもそのようなアプローチがよく記載されていたので、そこから引用してみる。
われわれは目的に注意をむけ、意力ををそそぎ、それを達成するための手段を考える。その目的のためにはどんな手段をえらぶべきか。いかにして目的に達しうるか。そういう結果をうるにはどうすればよいか。そういう結果にどこかで出あったことはないか。そのためによくつかわれる手はなにか。そうして同じか又はよくにた未知数をもつ、よくしれた問題を思いだせ。そうして同じか又はよくにた結果を得ようと努力せよ。 ( 140P 未知のものをよくみよ )
定義にかえるとか、分解と結合とか、補助要素 を導入するとか、一般化、特殊化とか、類推のように問題の変形にはいろいろな仕方がある( 152P 問題をいいかえることができるか )
条件のいろいろな部分を分離してその一つ一つしらべるが有利である。問題がむずかしい場合にはそれを更にこまかく分けてしらべなければならない。即ち或る言葉の定義にかえってそれにふくまれる新しい要素をみちびき、その要素をしらべることが必要である。 ( P45 分解と結合 )
このことから、問題を部分に分解して解を貼り合わせるということは、ざっくりと次のような観点である問題を捉えることとする。
- 今までに経験した中で似たような問題がなかったかを確認し、それが今回の問題に使えないか検討する
- 与えられた問題は、別の形に言い換えてみる。問題が示す条件のいろいろな部分を分離してみたり、過去に使ったことがあるようなものを補助的な要素として導入してみたりして、より簡単な問題の組み合わせに変形する
前に使ったあの関数の実装からこの部分は拝借して・・、とか、この機能を満たす条件は3つあるので、機能を3つに分解してみて・・、結局この機能を言い換えると、こんな機能の組み合わせになります、という考え方ができればよい。
モジュール化のサンプル
3 関数の貼り合せ にあるプログラムを Haskell で記述しながら、モジュール化を確認してみる。(はじめの一例)
-- | 引数のリストの要素を足しあげる
sum' :: Num a => [a] -> a
sum' = reduce add 0 where
add x y = x + y
-- | 第3引数のリストの各要素に対して f を適用する(畳み込む。foldrのこと)。初期値は x とする
reduce f x [] = x
reduce f x (head:rest) = f head (reduce f x rest)
sum'が汎用的な部分と、sum'に固有な部分とに分割できた。
このうちreduceについては、次のように再利用することができる
-- | 引数にとるリストを掛け合わせる
pruduct = reduce (\x y -> x * y) 1
-- | 引数にとるリストのいずれかがTrueならばTrue
anyTrue = reduce (\x y -> x || y) False
-- | 引数にとるリストのすべてがTrueならばTrue
allTrue = reduce (\x y -> x && y) True
-- | 引数にとるリストを連結する
append :: [a] -> [a] -> [a]
append a b = reduce (:) b a
-- | リストのすべての要素を2倍する
doubleAll :: Num a => [a] -> [a]
doubleAll = reduce doubleAndCons [] where
doubleAndCons = fandCons double
double = (2 *)
fandCons f = (:) . f
-- | doubleAll は、最終的にこのように書ける
doubleAll' = map' double where
double = (2 *)
map' f = reduce ((:) . f) []
doubleAll' が汎用的な部分と、doubleAll' に固有な部分とに分割できた
このうち map' については、次のように再利用することができる
-- | 行列を足しあげる
sumMatrix = sum' . map sum'
本文では、上のコードのうちreduce
関数と何か、またはmap'
関数と何かを組み合わせているときに、特にモジュール化 できた、またはモジュール化を深める、といった表現をしている。
まとめ
そもそもオブジェクト指向プログラミングにせよ関数プログラミングにせよ、複雑な問題を部分に分解することで取り掛かりやすくするという考え方は同じだと思っている。一般にそれをモジュール化と表現することもあらためてわかった。
大事なことは、オブジェクト指向プログラミングに比べて関数型言語は、そのモジュール化に際して、規約や規則の遵守による分解ではなく、本質的に問題を部分に分解しやすい(または、そう考えやすいコーディングができる)ということなのだろう。
ここからは個人的な決め事となるが、そうした本質的なモジュール化について少なくとも関数プログラミングを学ぶ上では次のように整理しておきたいと思う。
プログラミングにおける 問題を部分に分解してから解を貼り合わせることでモジュール化する とは、
- 機能の目的を整理して、与えられた条件を分解する(できるなら遅延評価の概念を活かした分解)
- 分解した単位で、次の工夫を施す
- 過去に使ったことのある汎用的な関数や標準ライブラリの関数を探す(利用する高階関数の発見)
- どうしても局所化・特殊化が必要な機能を関数は自分で定義する
- 分解したものを組み合わせて、目的の関数を実装する(実現したい機能について、より易しく言い換えて表現できているか確認)
- (加えて)新たにできた汎用的な関数は、ある機能を抽象化しているので、異なる局面でも活用可能な部品として使えないか検討する
という理解で学習・実践を進めていきたいと思う。