関数型言語 C++

C++ は JS ほどじゃないにせよ、誤解されてきた言語だと思います。いや、複雑だとかいうのは誤解ではなく紛うことなき真実なのですが、その複雑さがただパラノイアから来た複雑なだけのものではなく、一応の有用性を伴うものだ…っていやそれはわかってそうな気はするので、誤解というほどのことでもないわけですが、昨今に比べて、なんか有用そうなものである、という認識は低めだったように思います。

ただ今度は、なんか関数型な雰囲気らしーぜ、というだけの単語が一人歩き気味な気がします。なんかわからんけど C++ すげー、みたいな、一昔前の Haskell が得ていたようなイヤな地位をゲットしているような気がして、それはそれで別の種類の、ある種の誤解であるように思ったのでした。

というわけでマルチパラダイム言語 C++ について、関数型言語としての機能について僕の見方を。

まず絶対に意識して欲しい部分なのですが、というかここが一番誤解されつつあるような気がする部分なのですが、 C++ は 2種類の関数型言語を持ってる言語だと思います。コンパイルタイムに走る関数型言語と、実行時の関数型プログラミングは、全然別ものだと思います。ひとつひとつ見ていきましょう。

実行時については C++ で SICPコンパイル時については C++ for Haskeller なんかでサンプルとかが豊富なのでそんな感じで。というかこれ書いてる最中に後者を見て、はわわーと思ってぼんやりしてたら前者も出てきたので、説明不足でもこの2つにリンクはっときゃいいよねー、とかいいかげんに思ったからここにこの文書をはることにしたのでした。

基本的なこと

実行時はよく知ってるという前提で。

コンパイル時には、昔書いたのですが、

  • 値の代入 == typedef
  • 関数の引数 == template 引数
  • 関数の発動 == template クラスのインスタンシエイト
  • 関数の戻り値 == template クラス内での typedef

という図式を叩き込んでおくといいと思います。詳しくは wo さんの文章で。 wo さんの言う、「関数の構文はどうかしてる」のは全く完全に事実ですが、「C++ の全ての関数の構文がどうかしてる」のじゃなくて、「C++コンパイルタイムに走る言語の関数の構文がどうかしている」のであって、その理由は、もともとそんなことするために用意された機能じゃないからに決まっています。ですから、 wo さんの文章を見て C++ってなんてややこしい言語だろう というのは、私の感覚では少し違う。 C++コンパイルタイムに走ってる言語ってなんてややこしい言語だろう、なら納得できます…とか些細なことにこだわっている私はパラノイア言語に十分毒されているということが、まさに今証明されました。

そんでさて、コンパイルタイムに走る言語は、純粋関数型です。副作用は一切ありません。整数定数と型を計算するだけの言語です。 コンパイルエラーで計算結果が表示できる っていう認識じゃなくて、副作用が無いため、コンパイルエラーで型名を出力する以外に出力手段が無い、という認識が、今にして思えば正しかったように思います。ちなみに D は文字列や浮動小数も出力に使えます。

スタックトレース

残念ながら実行時には自動的には出てくれない処理系がほとんどです。デバッガなどを使うことになります。

VC6 とか以外は、コンパイルスタックトレースがサポートされていることが多いです。100万行を越えるエラーメッセージが出たなどと騒いだ私はただのアホだと思います。とんでもないレベルで再帰してたからスタックトレースがデタラメに長かっただけです。

コンテナ

実行時には、 STL という非常に強いパラノイアに支えられたライブラリが使えます。これも wo さんが指摘してましたが、文字列含めて全てリストだから文字列への強力な関数が全てのリストにも使える、という抽象レベルを一歩進めて、コンテナの外部イテレータに対して関数群を用意して、約束ごとにあったイテレータを使うのであれば、ヒープメモリの固まりだろうが文字列だろうがリストだろうがハッシュだろうがユーザが定義したコンテナだろうが、なんでも同じアルゴリズムが適用できる、という作りになってます。

ここで C++ の最もイカ(レ|シ)ているところは、 C のポインタもイテレータとして扱えるように、さらには C のポインタのインターフェイスにあわせて C++イテレータをデザインしたことかと思います、つまり、 ++ と -- が奇妙な方法でオーバーロードされます(前置と後置をどうやって区別するか考えてみて下さい)。

他には、入出力などもイテレータで抽象化されています。例えば以下のようなコード:

    vector<int> ints;
    ints.push_back(1);
    ints.push_back(2);
    copy(ints.begin(), ints.end(), ostream_iterator<int>(cout));

なんと copy というコンテナからコンテナにコピーするアルゴリズムで出力することができます…あまりうれしくないですね!

また、実は同じアルゴリズムを全てのコンテナに同様に適用することはできません。例えば上記の ostream_iterator を sort するなんてことはできません。それぞれのイテレータは concept という概念で分類されていて、 concept があわないアルゴリズムは適用することができません。しかし、悲しいことに concept の不適合チェックは言語レベルではサポートされていません。よって間違ったアルゴリズムを適用してしまった場合はわけのわからないエラーメッセージと戦うことになります…が、まぁそんなのはハスなんとかで型間違った場合とたいして変わらないのでまぁ些細なことなんじゃないのでしょうか。

コンパイル時にも Boost.MPL という極めて強いパラノイアに支えられたライブラリによって、 STL コンテナ的なものはたいていサポートされています。ですが、なんせ副作用の無いコンパイル時ですから、どうせ再帰中心でプログラムを書くことになるので、単純なリストの方が扱いやすいような気もします。

ラムダ

実行時には、 Boost.lambda というものを使うとラムダが使うことができます。

コンパイルタイムでは Boost.MPL.lambad なんだろうけど使ったことないです。

あと、 C++0x ではステキな記法でラムダが言語レベルサポートされるかも、という噂がありますね。

カリー

STL の bind1st, bind2nd などは不十分ですが、 Boost.bind が完全に役割りを果たします。というか、カリーとか言って、関数の後ろっかわしか部分拘束できないのはそれでいいんでしょうか。他の関数型言語と違って bind は引数や順序を自由自在にいじれます。例えば

void f(int x, int y);

っていう関数があったら、普通に後ろの引数を拘束するのも

function<void (int)> g(bind(&f, _1, 1));

前を拘束するのも

function<void (int)> g(bind(&f, 1, _1));

なんとなく順番を入れ替えることも可能です。

function<void (int, int)> g(bind(&f, _2, _1));

それラムダでできるよ、っていう話もありますが。

MPL はパラノイアさんが作ったのでこのへん全部あります。

多相

困ったことに何書こうとしてたのか思い出せません。

型推論

実行時には残念ながら無いです。 C++0x では右辺値から即わかる型名は省略できるようにしよかーという話があるみたいです。関数型言語にある型推論とは少し動機が違って、 template のせいでえらい長くなる型名を省略できるっていうような動機だと思います。

コンパイル時はやりたい放題です。ある意味究極の型推論というか Duck typing というか、意味的にぶっこわれない限りにおいては、どんな型にどんな型つっこんでも構いません。ぶっこわれた場合はわけのわからんエラーメッセージが出ます。(woさんの言う「出現評価」かな?)

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h