関数型プログラミングの誇大宣伝にうんざりしていませんか?私もです!今回は、私たちのような分別のある人間が関数型プログラミングに手を出さない方が良い理由として、いくつか不満を述べてみたいと思います。
はっきりさせておきたいのですが、ここで言う「静的に型付けされた関数型プログラミング言語」とは、型推論、デフォルトでの不変性なども含んだ言語のことです。実際には、HaskellやMLファミリー(OCamlやF#を含む)がこれに当たります。
理由1:最新の流行には乗りたくない
ほとんどのプログラマーと同じように、私は生まれつき保守的で、新しいことを学ぶのは好きではありません。だから私はIT業界で働く道を選びました。
私は、「クールな人たち」がやっているからといって、最新の流行に飛びつくことはありません。私は、物事が成熟し、ある程度の展望が得られるまで待ちます。
私にとって、関数型プログラミングは、まだ定着していると言えるほど長い間存在しているとは思えません。
確かに、一部の学者気取りは、MLやHaskellはJavaやPHPのような昔ながらの言語とほぼ同じくらい長い間存在していると主張するでしょう。しかし、私はHaskellのことを最近まで聞いたことがなかったので、その議論は私には通用しません。
そして、このグループの末っ子であるF#を見てください。まだ7歳ですよ!確かに、地質学者にとっては長い時間かもしれませんが、インターネットの世界では、7年なんてほんの一瞬です。
結局のところ、私は慎重なアプローチを取り、この関数型プログラミングというものが定着するのか、それとも一過性のものなのかを見極めるために、数十年待つのが良いと思います。
理由2:行数で給料をもらっている
皆さんはどうかわかりませんが、私はコードを書けば書くほど、生産性が高いと感じます。1日に500行のコードを書き出せれば、それは立派な仕事です。 私のコミットは大きく、上司は私が忙しく働いていることがわかります。
しかし、関数型言語で書かれたコードと、昔ながらのC言語のようなコードを比較してみると、コードの量がはるかに少ないので、怖くなってしまいます。
たとえば、使い慣れた言語で書かれたコードを見てみましょう。
public static class SumOfSquaresHelper
{
public static int Square(int i)
{
return i * i;
}
public static int SumOfSquares(int n)
{
int sum = 0;
for (int i = 1; i <= n; i++)
{
sum += Square(i);
}
return sum;
}
}
これを以下のコードと比較してみてください。
let square x = x * x
let sumOfSquares n = [1..n] |> List.map square |> List.sum
17行がたった2行になっています。この違いがプロジェクト全体に及ぶことを想像してみてください!
もし私がこのアプローチを採用したら、私の生産性は劇的に低下するでしょう。申し訳ありませんが、私にはそんな余裕はありません。
理由3:波かっこが大好き
もう一つあります。これらの言語はすべて波かっこをなくしていますが、どういうことなのでしょうか。本当にプログラミング言語と呼べるのでしょうか?
例を挙げて説明します。これは、おなじみの波かっこを使ったコードサンプルです。
public class Squarer
{
public int Square(int input)
{
var result = input * input;
return result;
}
public void PrintSquare(int input)
{
var result = this.Square(input);
Console.WriteLine("Input={0}. Result={1}", input, result);
}
}
そして、これは似たようなコードですが、波かっこがありません。
type Squarer() =
let Square input =
let result = input * input
result
let PrintSquare input =
let result = Square input
printf "Input=%i. Result=%i" input result
この違いを見てください!皆さんはどう思われるかわかりませんが、私は2番目の例を見ると、何か重要なものが欠けているような気がして、少し不安になります。
正直なところ、波かっこの指針がないと、少し迷ってしまうような気がします。
理由4:明示的な型を見るのが好き
関数型言語の支持者は、型推論によって型宣言をいちいち書く必要がなくなり、コードがすっきりすると主張しています。
しかし、実際のところ、私は型宣言を見るのが好きなのです。すべてのパラメータの正確な型がわからないと、不安になります。だからJavaは私のお気に入りの言語なのです。
これは、あるML風のコードの関数シグネチャです。型宣言は必要なく、すべての型は自動的に推論されます。
let GroupBy source keySelector =
...
そして、これはC#で書かれた同様のコードの関数シグネチャで、明示的な型宣言があります。
public IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
IEnumerable<TSource> source,
Func<TSource, TKey> keySelector
)
...
少数派かもしれませんが、私は2番目のバージョンの方がはるかに好きです。戻り値の型がIEnumerable<IGrouping<TKey, TSource>>
であることを知ることが重要なのです。
確かに、コンパイラが型チェックを行い、型が一致しない場合は警告を出してくれます。しかし、自分の頭でできることを、なぜコンパイラにやらせるのでしょうか?
確かに、ジェネリクスやラムダ、関数を返す関数など、新しいものをたくさん使えば、型宣言は非常に複雑になります。そして、それを正しく入力するのは本当に難しくなります。
しかし、私には簡単な解決策があります。ジェネリクスを使わず、関数をあちこちに渡さないことです。そうすれば、シグネチャはずっとシンプルになります。
理由5:バグを修正するのが好き
私にとって、厄介なバグを見つけて退治するスリルに勝るものはありません。そして、そのバグが本番システムにあれば、なおさらです。なぜなら、私はヒーローになれるからです。
しかし、読んだところによると、静的に型付けされた関数型言語では、バグを混入させるのがはるかに難しいそうです。
それは残念です。
理由6:デバッガの中で生きている
バグ修正の話が出たついでに言うと、私はほとんどの時間をデバッガの中で、コードをステップ実行して過ごしています。そう、ユニットテストを使うべきだということはわかっています。しかし、言うは易く行うは難し、ですよね?
とにかく、どうやらこれらの静的に型付けされた関数型言語では、コードがコンパイルできれば、通常は動作するようです。
最初に型を一致させるのに多くの時間を費やす必要があると言われていますが、それが終わってコンパイルが成功すれば、デバッグする必要はありません。そんなの面白くないですよね?
ということは...
理由7:細部まで考えたくない
型を一致させたり、すべてが完璧であることを確認したりするのは、私には面倒に思えます。
実際、あらゆるエッジケースやエラー条件など、起こりうるすべての問題を考慮しなければならないそうです。 そして、これを最初からやらなければならないのです。怠けて後回しにすることはできません。
私はむしろ、(ほとんど)すべてをハッピーパスとして動作させ、バグが発生したら修正する方が好きです。
理由8:nullチェックが好き
私はすべてのメソッドでnullチェックをするようにしています。その結果、自分のコードが完全に鉄壁であると知るのは、大きな満足感です。
void someMethod(SomeClass x)
{
if (x == null) { throw new NullArgumentException(); }
x.doSomething();
}
はは!冗談です!もちろん、nullチェックのコードをどこにでも入れるなんて、面倒なことはできません。そんなことをしていたら、本当の仕事は何もできません。
しかし、null参照例外が原因でひどいクラッシュを経験したのは、たった1回だけです。そして、私が問題の調査に費やした数週間の間、会社はそれほど大きな損失を出しませんでした。だから、なぜこれがそんなに大騒ぎされるのか、私にはよくわかりません。
理由9:デザインパターンをどこにでも使うのが好き
私はデザインパターンについてデザインパターン本で初めて知りました(なぜか「Gang of Four」と呼ばれていますが、理由はよくわかりません)。それ以来、私はあらゆる問題に対して常にデザインパターンを使うようにしています。確かに、私のコードは本格的で「エンタープライズらしい」ものになり、上司を感心させます。
しかし、関数型設計ではパターンについて言及されていません。Strategy、AbstractFactory、Decorator、Proxyなどを使わずに、どうやって役に立つことを成し遂げられるのでしょうか?
もしかしたら、関数型プログラマーはこれらのことを知らないのでしょうか?
理由10:数学的すぎる
2乗和を計算するためのコードをもう一つ紹介します。これは、奇妙な記号がたくさん使われているため、理解するのが非常に難しいです。
ss=: +/ @: *:
おっと、失礼!これはJのコードでした。
しかし、関数型プログラムでは<*>
や>>=
のような奇妙な記号や、「モナド」や「ファンクター」と呼ばれる難解な概念が使われているそうです。
なぜ関数型の人たちは、私がすでに知っているもの、つまり ++
や !=
のようなわかりやすい記号や、「継承」や「ポリモーフィズム」のような簡単な概念を使いつづけてくれなかったのでしょうか?
まとめ:理解できない
結局のところ、私は理解できません。なぜ関数型プログラミングが便利なのか、私にはわかりません。
私が本当に望んでいるのは、誰かが1ページで本当のメリットを教えてくれることであって、情報を詰め込みすぎることではありません。
更新:それで、私は「1ページですべてがわかる」ページを読みました。しかし、それは私には短すぎて、単純すぎます。
私はもっと深く掘り下げられるもの、歯ごたえの ある ものを求めているのです。
だからといって、チュートリアルを読んだり、例題で遊んだり、自分のコードを書いたりするべきだなどと言わないでください。私はただ、そういう苦労をせずに理解したいだけなのです。
新しいパラダイムを学ぶためだけに、自分の考え方を変えたくはありません。