関数型言語ってなに?
ほんとこれ。
関数型言語って何??オブジェクト指向ですらよく分かってないのーーー!!!アセンブリ!!アセンブリ言語とは違うのーーー!????
— からあげon the レモン (@karaage0703) 2014, 12月 29
僕も、ピュアな関数型言語は7年前にCommon Lispに触ったきりご無沙汰です。
というわけで、IT用語辞典で調べてみました。
すべての計算や処理などを関数の定義の組み合わせとして記述していくタイプのプログラミング言語。
「同じ入力には必ず同じ出力を返す」「関数の評価が他の関数に影響を及ぼさない」など数学における関数と似た性質を持った関数の定義としてプログラミングを行い、プログラムの実行は記述された関数群の評価として行われる。
なるほど、わからん。
ちょっと説明が抽象的すぎるので、具体例を以下に示します。
「同じ入力には必ず同じ出力を返す」
たとえば のように、 が決まれば も一意になる関数などはこの性質に当てはまります。
一方、ランダムな値を返すrand()
や、現在の日時を返すnow()
などは、実行のたびに結果が異なるので失格です。
あと、処理が成功したか失敗したかによって結果が変わる関数*1も、この性質には当てはまらないと言えます。
「関数の評価が他の関数に影響を及ぼさない」
これはちょっとイメージしづらいかも知れません。
プログラムの中にhoge()
とfuga()
という関数があるとしましょう。
で、hoge()
の後にfuga()
を実行したときと、fuga()
だけを単体で実行したときで処理結果や内部状態が変わらない性質のことを指しています。
こういう性質を、一般的に「副作用がない」と表現したりします。
参考までに副作用のあるJavaプログラムの例を以下に示します。
public class Sample { private static int x; public static int hoge(int param) { return x *= param; // ←ここ } public static int fuga(int param) { x += param; // ←ここ return param; } }
上の例では、関数(まぁメソッドですが)を呼ぶたびにフィールドが書き換わってしまうので、そのフィールドを参照するすべての関数にも影響が及びます。
ここまで理解したところで、そもそもなぜ今さら関数型言語が持て囃されているのかを考えてみます。
なぜいま関数型言語なの?
おそらく関数型の思想で書かれたコードはテストがしやすいからだと思うのです。
最近の開発では、xUnit というテスティングフレームワークを用いたユニットテストが一般的になってきました。
これらのテスティングフレームワークは、一般に以下の性質を持っているコードと特に相性がよいとされています。
- 同じ入力パラメータに対して、必ず決まった出力が得られる*2
- 副作用を持たない(クラスの内部状態を変化させない)
なんと、先ほどの「IT用語辞典」の説明と一致するではありませんか。関数型言語の性質がそのままテスタビリティに直結するというかけがえのない利点があるというわけです*3。
そう。人々は、テスタビリティの高いコードを追い求めるうちに、あることに気づいてしまったのです。
「関数型プログラミングこそ至高」
という、恐るべき事実に。
まぁこれは大げさですが、テストを自動で行うにせよそうでないにせよ、こういう関数型プログラミングのエッセンスを取り入れた設計はシンプルで美しいという思想がやんわりと支配的になりつつあります。
そして、その流れはついに.NetやJavaなどが採用していたパラダイムまで変えてしまいました。
そう、ぼくらが大好きなJavaにまで関数型プログラミングの思想が忍び寄っていたのです!!
Java、彼の場合(ラムダ式編)
関数型言語の主役は、ずばり「関数」です。そのままですね。
小さな関数をうまく組み合わせて、大きな処理を実現しようという考え方です。ちがったらごめん。
一方、オブジェクト指向言語Javaの主役は、ずばり「クラス」でした。オブジェクト指向という思想が現れた20余年前は、システムの仕組みを「オブジェクト同士のメッセージのやり取り」になぞらえて設計していたそうです。
Javaは「一連のアルゴリズムの中で、処理の一部をほんのちょっとだけ変えたい」という柔軟さに弱く、小回りの利きづらい言語でした。
たとえば、Javaのソート処理を考えてみましょう。ソート順は「文字数の多い順」であるとします。
// 文字数の多い順に並べ替えたい List<String> strList = new ArrayList<String> (Arrays.asList("****", "**", "***", "*"));
これを今までのやり方で実現しようとすると、ひとまずソートに必要な比較関数 compare()
をクラスで一枚包んで、List#sort()
にクラスごと引き渡す必要がありました。
class StringCompalatorDesc implements Comparator<String> { public int compare(String s1, String s2) { return Integer.compare(s2.length(), s1.length()); } }
strList.sort(new StringCompalatorDesc());
// 匿名クラスを用いた別解 strList.sort(new Comparator<String>() { public int compare(String s1, String s2) { return Integer.compare(s2.length(), s1.length()); } });
処理の制御のために関数ひとつを指定するだけのために、なにがなんでもクラスの形にしてやらねばなりませんでした。たったひとつの、その場かぎりの関数を指定するためだけに。わざわざ。クラスを。
もしも関数をクラスで包まずに、そのまま引数に渡すことができたら、きっと無駄なクラスが減ってプログラムはすっきりしそうですね。
かくして、Java 8からは以下のように引数に関数(っぽいもの)を指定できるようになりました。マンモスうれぴー。
strList.sort((String s1, String s2) -> Integer.compare(s2.length(), s1.length()));
ちなみに、この引数部分の
(String s1, String s2) -> Integer.compare(s2.length(), s1.length())
みたいな式は、「ラムダ式」と呼ばれています。
実はこれも元々は関数型言語特有の用語でしたが、今ではC#、VB、C++などのメジャーな言語にもレギュラー入りしたキモい構文です。
なにゆえJavaはこんなキモい構文を導入してまでラムダ式を採用したかったのでしょうか。
ラムダ式は遅延評価
ラムダ式には遅延評価されるという大きな特徴があります。
不正確を承知でざっくり言うと、遅延評価とは「処理内容を前もって予約しておき、時がきたら実行する(その時がくるまで実行しない)」という性質のことです。
「ボタンが押されたら◯◯の処理をしたい」「アルゴリズムの中で××の箇所に差し掛かったら処理◯◯を実行したい」「ダウンロードが完了したタイミングでユーザに通知するため◯◯の処理がしたい」というとき、前もって◯◯の処理を予約しておくのです。
先ほどのソートの例でも、sort()
呼び出しのタイミングで比較関数をラムダ式の形で引数に記述することで、しかるべきときに実行するよう予約しています。
今までのJavaでも一応こういうことはできました。単にクラスが主役か関数(メソッド)が主役か、どっちがコードをすっきり書けるかというだけの話だと思っていて、オブジェクト指向も関数型もそこまで相反する概念ではないというのが僕の思うところです。
ですが、関数が主役になると色々便利なんですよ。
ちょっと次の例をご紹介しますね。
Java、彼の場合(Stream編)
いきなりですがナイスな例題。
以下のリストの全要素を標準出力にプリントしたいとしましょう。
// リストの全要素を出力したい List<String> strList = new ArrayList<String> (Arrays.asList("こぶた", "たぬき", "きつね", "ねこ"));
はい。
Javaを習いたての人は、たぶんfor
文と変数i
を使って以下のように書くでしょう。
for(int i = 0; i < strList.size(); ++i) { System.out.println(strList.get(i)); }
// 別解 for(Iterator<String> i = strList.iterator(); i.hasNext(); ) { System.out.println(i.next()); }
最も一般的な書き方はこうでしょうか。
// 別解② for(String elements : strList) { System.out.println(elements); }
このfor
文による配列の走査は、初級プログラミングの超頻出テクニックです。
ですが、本当にやりたいことは配列の走査そのものではなく、走査によって得られた要素の出力のはずです。
「なにを言ってるんだコイツは」と思われるかもしれませんが、上記のコードには
- 配列を走査して要素をひとつずつ取り出すこと
- 取り出された要素に対して加工(出力)すること
という、本来ならば目的の異なる2つの処理が混在してしまっているのです。
本来、この2つは分離しなければならないのですが、今までのJavaの言語仕様による制約のせいでやむを得ず上記のようなコードになってしまっていたのです((余談ですが、Common Lispでもよほどの事情がないかぎりfor
は使わず、再帰呼び出しによって反復処理を実現し、関数の中で要素に対する処理を書きます。))。
しかし、Java 8では満を持して「リストの走査」と「個々の要素に対する処理」を分離することができるようになりました。
それがこちら。
strList.forEach((String str) -> System.out.println(str));
// 別解
strList.forEach(System.out::println);
なんということでしょう。
たった1行です。
Javaは関数型に毒されたおかげで、forEach()
に渡すラムダ式(または参照メソッド)によって走査中に得られた要素に対する処理が可換になり、関心の分離をより柔軟に行えるようになりました。
あれ?
これはオブジェクト指向プログラミングにおける「変わらない部分から変わる部分を切り出す」操作そのものです。
そう、関数型はオブジェクト指向と対をなすパラダイムではなく、融合とシナジーにより愛と勇気をもたらす夢の思想だったのです。これらはいつでも2人で1つだった。地元じゃ負け知らず。
そんなこんなで強引なまとめ
だいぶ脱線した気もしますが、まとめ。
- 関数型言語の方針で設計するとなんか美しい
- 方針1. 同じ入力には必ず同じ出力を返す
- 方針2. 関数の評価が他の関数に影響を及ぼさない
- メソッドが主役になるとコードがシンプルになる
- 関数オブジェクトではなくラムダ式で済む
- 本来の関心事のみに対して集中できる
だから恐れないでみんなのために。たとえ胸の傷が痛んでも。
おまけ
ちなみに、からあげさんのHello Worldはmain()
関数の型やreturn
文を省略するなどかなりの高等テクが駆使されています。
取り乱してお見苦しいところをおみせしました。お詫びにハローワールドしますね
#include <stdio.h>
main()
{
printf("hello, world");
}
— からあげon the レモン (@karaage0703) 2014, 12月 29