えちょ記

語らないブログ

関数型言語で工数削減できる理由、前編

関数型言語は開発効率が良い」とよく言われます。「オブジェクト指向と比べて‥‥」なんてつい比較してしまうがゆえに論争っぽいループが発生したりするのを良く見かけますが、まあそれはおいといて、実際なぜ効率が上がるのか考えてみました。
関数型言語が採用する概念のうち、特に工数削減に貢献する要素を挙げてみます。

パターンマッチ

プログラムでは条件に応じて処理を仕分けることが多々ありますが、入り組んだIF文はそれだけでプログラムの意図が分かりにくくなりバグの温床となります。
モダンな関数言語では、条件判断記述を関数の入り口に設置し、条件を満たしたときだけ関数の本体を実行するような構文が書けるタイプの文法を採用しています。このような構文は、コンパイル時或いは実行時にIF文が合成され、パラメータにより処理が分岐されます。CやJavaなどのswitch-case構文をものすごく強力にしたもの、と考えればいいと思います。
特にList構造に対するパターンマッチが存在するタイプのものは末尾再帰と合わせてコードの可読性に大きな貢献をします。

末尾呼び出しの最適化

関数型言語の多くはループ構文がありません。C言語系などからプログラムを覚えた人が真っ先に混乱する要素です。処理のループを実現したいときは最後のパラメータを引数にした自分自身を呼び出す再帰処理を組むことで実装します。
「スタックが溢れるじゃないか!」と思いますが、特定の条件を満たす場合、スタックを消費しない再帰呼び出しに最適化可能です。これが末尾呼び出しの最適化と呼ばれるもので、「関数の処理の一番最後で他の関数*1を呼び出す」という条件が必要です。
結局、コンパイル後の処理はループになっているわけで、なぜわざわざループを処理系に作らず再帰で書く必要があるのだ?と、やはりC言語系から入った人は思ってしまう拒絶反応の理由となるのですが、(構文からの)ループの排除はやはり後述する「変数の再代入不可」など関数言語特有の要件を満たすための重要な仕様となっています。
ループで書くか再帰で書くか、コーディングや読みやすさの手間としては結局同程度です。末尾再帰の導入は関数言語の利点を享受するために必要なルールなのだと解釈するのが吉でしょう。

変数の再代入不可

モダンな関数型言語では、一旦変数に代入した値は変更できません。=(イコール)は、代入式ではなく、数学的な=(等式)と解釈します。C言語的に言えば、全ての変数の宣言にconst int X = 10;などとconstをつけるようなものです。
ああなんて面倒なんだ、と思うのは、やはり手続き型言語から入ってしまったからそう思っちゃうんですよね。しかし再代入不可という特徴は、デバッグを行うときに最大限の威力を発揮します。
なお、再代入不可の文法で一番困るのは直前の値を引き継ぐタイプのループ構造を書く場合です。というか書けません。そのためループ処理を行う場合、計算結果を新しい値として自分自身を呼び出す末尾再帰でループの代わりとします。前述の通り、関数型言語では末尾再帰は最適化されるため、再帰によるオーバーヘッドは発生しません。

高階関数

「こうかいかんすう」です。MS-IMEで変換するときは「たかしなかんすう」と書きましょう。‥‥というのは単なる小ねた。
関数を引数として受け取る関数のことをこのように呼びます。C言語的にはコールバック、もっとモダンな言語ではデリゲートと呼ばれるものを受け取る関数のことです。高階関数では「関数の合成」を行います。
というか、まず利点から。高階関数がある処理系では、「分かりやすい単位で処理を分けた関数を用意して、後で組み合わせることができる」のです。よくあるまずいコーディングに、似たような処理があちこちにある、というものがあります。このような場合は基本的に機能単位に関数を分けることで対処しますが、高階関数の仕組みを理解して適切に利用することで、個々の関数の仕組みを使いまわせる場面が飛躍的に増えます。

*1:最初は「自分自身」としていましたが間違い。最後に呼び出すならあらゆる関数が最適化対象です。