プログラミング学習サイト

プログラミングの学習を開始される方を対象としたプログラミング入門サイトです。

関数型プログラミングとは何か?【Land of Lisp】

lispが高い評価を受けていたのは、研究の道具として計算機科学の世界で最も複雑な種類の問題を解くために使われていたことだ。

今回はlispの進んだ要素の一つ、関数型プログラミングについて学ぶ。

関数型プログラミングとは何か?

ものすごく大雑把に言えば、関数だけでプログラミングを組み立てるスタイルのことだ。 しかし、ここでの関数には通常のプログラミングよりもさらに特化された意味がある。 数学者が使う「関数」という言葉と全く同じ意味で使っているということだ。

大学数学では次のように関数を定義している。

定義(関数・写像

集合

A の全ての要素に対し,その要素を入力すると,集合

B の特定の要素をただ一つ出力するものを関数 (function) または写像 (mapping) という。

このとき,

A をその写像の定義域 (始域; domain) といい,

B を終域 (codomain) という。

またこのとき,

A を入力したときの出力となり得る

B の部分集合全体を関数(写像)の値域 (像; range) という。

まとめると、関数には受け入れる値の集まり(定義域)があり、それぞれの値について関数が返す値が決まっているということだ

数学の関数には次のような性質がある。 そして、lispの関数もできるだけこの規則に従っているべきである。

  • 巻子は渡される引数が同じであれば、常に同じ結果を返す参照透過性
  • 関数は、外部の変数を参照しない
  • 関数の呼び出しが、変数の値を変えることはない(これが副作用)
  • 関数の目的は、引数から値を計算し、評価することのみである。
  • 関数は、世界に影響を与えない。ビープ音やポップアップを鳴らしたりはしない。(これも副作用と呼ぶ)
  • 関数は外の世界からの情報を受け取らない。例えばキーボードやハードディスクを読んだりはしない。

これらのルールに従って書かれたプログラムは関数型プログラミングと呼ばれる。

例えば、次のようなsin関数は関数型プログラミングに従っているといえる。

> (sin 0.5)
0.479425555

副作用について

プログラムのすべてのコードを関数型プログラミングのスタイルで書くことは不可能だろう。 ルールの中にはコンピューターが音を鳴らすことを禁止しているものもあるためだ。

プログラムコードが外の世界に影響を与えない限り、少なくともコンピューターを鳴らしたりスクリーンに文字を移したりといったことをしなければならない。

このような要素を、副作用と呼ぶ。

副作用のあるコードとは、命令型である。 「あれをせよ」「これをせよ」という形で書かれ、結果として世界のどこかに影響を与えている。

関数型プログラミングであっても、副作用とは上手に付き合っていかなければならない。(出なければ、関数型で書くプログラムは何の影響力もない計算機にしかならないからだ)

  1. コードの8割は関数型プログラミングで書き、副作用の発生する2割と隔離するべき
  2. 残りの2割で副作用のあるコードを書き、ユーザーおよび外の世界との干渉を担当するべき

(あるいは、グローバル変数の値を書き換えたり、引数の値を変更したりすることも副作用と呼ぶが、これは関数型プログラミングにとってはあまりよくない副作用だ。)

サンプルコード

;; Function to calculate the factorial of a number
(defun factorial (n)
  (if (<= n 1)
      1
      (* n (factorial (- n 1)))))

;; Function to calculate the sum of a list of numbers
(defun sum (lst)
  (if (null lst)
      0
      (+ (car lst) (sum (cdr lst)))))

;; Example usage
(print (factorial 5)) ; Prints 120
(print (sum '(1 2 3 4 5))) ; Prints 15

factorialsum関数は純粋な関数として、先ほど示した数学的な関数の要素を満たしている。

  • 巻子は渡される引数が同じであれば、常に同じ結果を返す参照透過性
  • 関数は、外部の変数を参照しない
  • 関数の呼び出しが、変数の値を変えることはない
  • 関数の目的は、引数から値を計算し、評価することのみである。
  • 関数は、世界に影響を与えない。ビープ音やポップアップを鳴らしたりはしない。
  • 関数は外の世界からの情報を受け取らない。例えばキーボードやハードディスクを読んだりはしない。

よって、これらの関数は関数型プログラミングスタイルで書かれているといえる。

加えて、コードで副作用が発生する部分は2割以下に抑えられている上に、関数型プログラミングスタイルとは隔離されている。

下のprint関数はディスプレイに計算結果を表示するという副作用を持っているが、2割以下に抑えられ、まとめられた箇所に凝縮してあるため管理できるだろう。

高階プログラミング

すべてのリストに2を足して新しく結果を表示するコードを書きたいとする。 経験の浅いlispプログラマーは次のようにコードを書くだろう。

(defun add-two(list)
    (when list
        (cons(+ 2 (car list)) (add-tow car list))))

(add-two `(4 7 8 3) )
> (6 9 10 5)

この関数は、確かに関数型プログラミングスタイルである。しかし、

  • 要素に2を足すコード
  • リストを走査するコード

これらのコードが混ざってしまっている。 結果として、add-twoという2を足すためだけに特化されたコードが生み出されてしまった。

経験のあるlispプログラマーは次のようにコードを書く。

(mapcar (lambda(x) (+ x 2)) `(4 7 8 3) )

このコードは

  • lambdaを使用して2を追加する関数の定義と呼び出しを同時に行っている。
  • mapcarで走査を行っている。

の二つを別の関数に役割を分散している。

それによって、add-two関数を定義する必要がなくなり、 コードが明瞭簡潔になっている。

関数型プログラミングのメリット

バグが少なくなる

プログラムのバグというのは通常、あるコードが特定の環境下で、プログラマがそれを書いたときに期待と違うふるまいをすることで生じる。

関数型プログラミングでは、関数のふるまいに影響を与えるのは引数だけだ。 同じ引数で呼び出せば何度実行しようと同じふるまいになるはずだから、再現性が高い。

数学に近い

関数型プログラミングの利点はコンピュータープログラミングを数学の領域に戻してくれる点だ。 ポップアップやダイアルボックスは数学とは無縁だが、それらに表示する内容を構成するまでの値変換は数学そのものだ。

プログラミングを数学と同じレベルの純粋さにまで戻せることができれば、すでに証明された数学の領域の公式やツールを使用することができる。

実際、lispの基礎にはラムダ三方という数学上の概念が元となっている。