deq blog このページをアンテナに追加 RSSフィード

2006-12-13 今日のひとこと『シュレ猫の遅延評価』,関数と演算子

[] 関数演算子

関数演算子についてちょっとだけ深く知りたい人のために。


ビンゴ中西のほげほげmalloc or calloc

ややこしいが sizeは具体的に10とかの数字で、

sizeof( ) は演算子であることに注意!


演算子なのに( )が付くのはPerlPHPと同じ配慮だと思っておこう・・・ 

実は PerlPHPでもどうして演算子に( ) が付くのが許されてるのか理解してないが・・・ 関数みたいにどうしても見えるから??

print とか echo とか 確か関数じゃないのに( )付けれるんですよね。。。 まあ、あったところで、特に問題ないように 思うのですが、、、 なにか 多くの書物で これらは 関数じゃないですけど ( ) を付けることを 許しています とだけあって、 その理由がのってないんですよねぇ。。。 たんに見た目にわかりやすいように というだけなんでしょうか・・・

そもそも僕には sizeof() のカッコはどちらかというとキャストで使われる (int) のカッコのように見えるのですが(「見える」だけで「思って」いるわけではないけれど),それは置いておきましょう。

まず,最も基本的なことを述べます。演算子関数は一緒のものです。本質的な機能に違いはありません。このことはLispなどの関数型言語を使っている人にとっては当然のことかと思います。

とはいっても,実用上,全く一緒というわけではありません。概念的には,演算子関数の一形態と考えると自然です。似たような概念に手続き(プロシージャ)もしくはサブルーチンと呼ばれるものなどもあるので,一緒に整理すると次の表のようになります。

名称 戻り値の有無記法(前置・中置・後置)
広義の関数 有り/無し 主に前置(その他,中置/後置など)
数学関数 有り 前置
演算子 有り 単項演算子は前置か後置,それ以外は中置
手続き / サブルーチン 無し 前置

ここで,「広義の関数」は他の3種類全てを含んでしまう概念です。ただし,これは(個人的主観の入った)典型的な分類なので,プログラミング言語によっては扱いが異なったりします。そこらへんは適宜読み替えてください。

これらの分類を説明しましょう。昔々Fortranという言語がありましたが*1Fortranには関数(function)とサブルーチン(subroutine)があります。Fortranに限らず,Basicという言語でも関数とサブルーチンがありますし,Pascalという言語では関数と手続き(procedure)と呼びますが,手続きとサブルーチンは同じものです。サブルーチン(手続き)というのは,単に一連の処理をまとめたものです。例えば(コード例はC言語で書きます),

/* グローバル変数を初期化する */
CurrentState = 0;
ItemCount = 0;
FlagIsPlaying = 0;

といったひとまとまりの処理に対して,

/* グローバル変数を初期化する */
void initialize_global_variables(void) {
  CurrentState = 0;
  ItemCount = 0;
  FlagIsPlaying = 0;
}

というように名前付けを行います。これは,特にプログラム中に同じ処理が2回以上出てきた場合に有効です。それに対して,関数数学で使われている関数を表現するものです。数学的な関数というのは,与えられた引数に対応する値を返すものです。引数はいくつでも構いません*2が,戻り値は1つです。例えば,f(x) = 3x + 5の値を求める関数

double f(double x) {

return ( (3 * x) + 5 );

}

となります。サブルーチンは「一連の処理をまとめて名前をつけたもの」,関数は「引数に対する値を求めるもの」です。ただし,意味的な違いはそうなのですが,実際の機能的な違いは「戻り値があるかないか」ということだけになります。というのも,実際はサブルーチンは引数を受け付けるようになっていますし(その方が便利ですよね),何らかの複雑な処理をする,という点では共通しているからです。戻り値があるかないかだけの違いならまとめてしまえ,サブルーチンを書くかわりに戻り値を持たない関数を書こうということで,例えばC言語では関数しかありません。なお,Fortran時代の関数は上記の表では数学関数と呼んでいます。これはC言語的な「広義の関数」に対して,狭義の関数とも呼べます。

余談ですが上記のサブルーチン的な性質に着目したのがCやJavaのような手続き型(命令型)言語で,関数的な性質に着目したのがLispHaskellのような関数型言語ですね。特に,数学関数のように,引数が決まればそれに対応する値が一つであることが保証されているような関数を純粋な関数と呼び*3,そのような関数しか作れない言語を純粋関数型言語と呼んでいます。

さて,次は演算子を見てみましょう。演算子 (operator) には項の数 (arity) =引数の数による分類というのがあって,具体的には単項演算子 (unary operator),2項演算子 (binary operator),3項演算子 (trinary operator) があります。4項以上の演算子というのは,探したらあるでしょうが,僕はいまだ見たことがありません。次の表はC言語で使われる演算子をいくつか抜き出したものです(C言語の全ての演算子の一覧は『 BohYoh.com【C言語講座】演算子一覧表 』などを見てください)。

演算子 名称 アリティ記法結合性優先順位
++ インクリメント演算子(後置)単項 後置 左結合 1
++ インクリメント演算子(前置)単項 前置 右結合 2
+ 加算演算子(単項) 単項 前置 右結合 2
* 乗算演算子 2項 中置 左結合 4
+ 加算演算子(2項) 2項 中置 左結合 5
? : 条件演算子 3項 中置 右結合 14
= 代入演算子 2項 中置 右結合 15

単項演算子の例としては ++ があります。これは a++ や ++a として使えます。引数が一つなので,前置記法か後置記法のどちらかです。それから,+ は単項演算子と2項演算子の両方に使われています。これは 3 + (+2) と書いた場合,「+2」の部分が単項演算子で,「3 + X」の部分が2項演算子です。引数が2つの演算子,つまり2項演算子の場合,通常は中置記法になります*4

ついでなので結合性と優先順位も一応説明しておきましょう。「3 + 5 * 2」と書いた場合,結果は13になってほしいと思います。ですから,「3 + 5 * 2」は「(3 + 5) * 2」ではなく「3 + (5 * 2)」として解釈されるべきです。これをルールにしたのが優先順位で,演算子の優先順位表に載っています。上記の表では * は + よりも優先順位が高い(表の上の方にあるのがたいてい高い優先順位です。ここでは数字が低いほど優先順位が高くなります。)ことになっているので,+ よりも * が先に「結合」して,まずは 5 * 2 が評価(計算)されます。

それから,3 + 4 + 5 といった場合,「(3 + 4) + 5」なのか「3 + (4 + 5)」なのかをはっきりさせる必要があります。どっちでもいいんじゃないかって?いえいえ,「3 / 4 / 5」の場合は困りますよね。「(3 / 4) / 5」と「3 / (4 / 5)」の結果は異なります。同じ優先順位の演算子が連なった場合,「3 / 4 / 5」が「(3 / 4) / 5」になるのを左結合(左から順に結合させていきます),「3 / (4 / 5)」になるのを右結合と呼びます。上の表には書いていませんが,C言語では / 演算子は左結合なので「3 / 4 / 5」は「(3 / 4) / 5」になります。

さて,本題です。「3 + 4 + 5」と書くと,3と4が足されて,その結果7と5がまた足されます。これは,+ 演算子引数を2つとって,値を一つ返しているといえます。ですから,+ 演算子は,

double add(double a, double b) {
  return a + b;
}

という関数を定義してあげれば,「3 + 4 + 5」を「add(add(3, 4), 5)」と関数で書き直すことができます。書き直せるということは,本質的に同じものだということです。異なるのは,演算子は前置・中置・後置などの記法で書けるのに対して,関数呼び出しは常に前置である点です。前置であれば,引数の数さえ分かっていれば結合のあいまい性がありません。例えば「3 + 4 + 5」を表した「add(add(3, 4), 5)」ですが,addが2引数関数であれば「add add 3, 4, 5」と書いたとしても,「add (add 3, 4), 5」であることが確実に分かります。とは言っても,たいていの言語では引数の数を決めうちしない(可変長引数の)関数も定義できるようになっているので,C言語などでは関数呼び出しには () を必ずつけるような規則になっています。

それから,演算子は必ず値を返します。演算子関数と同様に「引数を受け取って値を返す」ものなので,値を返さないと演算を行う意味が何もありませんから*5。それに対して,制御構造は値を返しません。3項演算子 ?: と制御構造 if-else を比べてみましょう。3項演算子 a ? b : c は,a が真なら b を返し,そうでなければ c を返します。ですから,

max = (a > b ? a : b);

という風な書き方ができます。それに対して,if-elseは値を持ちませんから,

if (a > b) {
  max = a;
} else {
  max = b;
}

と書く必要があります。

max = if (a > b) { a } else { b };

というような書き方はできません(C言語であれば構文エラーとなってはじかれます)。とはいっても,Rubyという言語では if-else 制御構造は(値を返さない)文ではなく(値を返す)式という取り扱いになっていて,

if a > b then
  max = a
else
  max = b
end

という書き方も

max = if a > b then a else b end

という書き方も両方できるようになっています。Rubyでは if-else と ?: の両方を覚える必要はなく,if-else-end という一つの語彙で十分なのですね。でも,こういう柔軟な記法を持った言語は少ないようです。

さて,ここまでくるといくつかの疑問に答える準備が出来ました。余計な話も多かったので長くなりましたけれども。最初の引用に戻りましょう。

ややこしいが sizeは具体的に10とかの数字で、

sizeof( ) は演算子であることに注意!


演算子なのに( )が付くのはPerlPHPと同じ配慮だと思っておこう・・・ 

実は PerlPHPでもどうして演算子に( ) が付くのが許されてるのか理解してないが・・・ 関数みたいにどうしても見えるから??

print とか echo とか 確か関数じゃないのに( )付けれるんですよね。。。 まあ、あったところで、特に問題ないように 思うのですが、、、 なにか 多くの書物で これらは 関数じゃないですけど ( ) を付けることを 許しています とだけあって、 その理由がのってないんですよねぇ。。。 たんに見た目にわかりやすいように というだけなんでしょうか・・・

まず,その話はPerlとは関係ありません。PHPの話です。勘違いですね。Perlに関しては後で少し詳しく述べましょう。PHPについては詳しくありませんが,言語のリファレンスマニュアルを読んで分かる範囲でお答えしましょう。

PHPの print や echo が関数ではない,というのには2つの意味があります。一つ目は,printは値を返します(常に1を返します)が,echoは値を返しません。言い換えれば,printは関数として振る舞いますがechoは関数として振舞いません。上でif-elseの例で述べたように,「$ret = print "hello";」とは書けても「$ret = echo "hello";」とは書けないのです(C言語と同じように構文エラー)。二つ目は,PHPでは関数呼び出しには必ず () を後置しなければなりません。しかし,print や echo は例えば print "abc"; としてカッコ無しでも使えるように,関数ではなく言語構造として定義されています。その場合,通常の言語であれば(特別な処理をしていなければ),print("abc"); のカッコは「関数呼び出しのカッコ」ではなく「結合順位の明示/変更のカッコ」として解釈されます。つまり,3 + 4 を (3) + (4) と書いているのと同様の意味です。あるいは,C言語で return x + 4; を return (x + 4); と書くのと同様の意味です。PHPもおそらく同様の処理をしているのではないかと推測されます。許されるか否かという点では,これが許されなければ return (x + 4) という記法も許されないことになりそうですが,わざわざそのような意味のない制約を加えることはないでしょう。

さて,話をメインストリームに戻します。先ほど,関数演算子は本質的に同じですが,関数は前置のみなのに対して演算子は前置・中置・後置などの記法があるところが違っていると述べました。実はもう一つ,機能的な差異ではありませんが,重要な違いがあります。それは,ほとんどのプログラミング言語において,関数プログラマが自由に定義できるけれども,演算子は自由に定義できないという点です。

C言語で階乗を求める単項後置演算子 ! を定義できるかというと,定義できません。 3! と書いて 3 * 2 * 1 を計算させることはできないのです。しかし,制約付きで演算子を定義できる言語も存在します。例えばC++D言語Rubyです。これらの言語では新たな演算子記号を作ることはできません(階乗を 3! で求めることはできません*6)が,既に存在している記号の演算子オーバーロード(多重定義)することができます(関数やメソッドのオーバーロードもできます)。

演算子関数)のオーバーロードとは,引数の型に応じて呼び出す処理(関数)を変える仕組みです。C言語数学関数に絶対値を求める関数があるのですが,ちょっと列挙してみましょう:

  1. int abs(int x) - int型の絶対値を求める
  2. long labs(long x) - long型の絶対値を求める
  3. double fabs(double x) - double型の絶対値を求める
  4. float fabsf(float x) - float型の絶対値を求める

ものすごくちからわざ感が伝わってきます。型が増えるたびに違う名前の関数を用意するのですね。これがC++では関数オーバーロードを利用して abs() だけになっています。簡単にいえば,double型のxでabs(x)と呼べば自動的にfabsが,float型のxでabs(x)と呼べば自動的にfabsfが呼び出される仕組みです。実際は,例えば

int    abs(int x)    { return (x < 0 ? -x : x); }
long   abs(long x)   { return (x < 0 ? -x : x); }
double abs(double x) { return (x < 0 ? -x : x); }
float  abs(float x)  { return (x < 0 ? -x : x); }

という感じのC++コードで定義されています(実装はもっと最適化しているかもしれませんが,オーバーロードの原理は同じはずです)。型の分だけ定義があるのはC言語のときと同じなのですが,呼び出す側は同じ関数名で呼び出せます。そうすると,C++コンパイラ引数の型から自動的に正しい関数を選択してくれます。便利ですね。

演算子オーバーロードは,上記例の演算子版です。例えば,2次元座標 (x, y) を表す Point という構造体があったとして,Point 型の変数同士を a + b + c という感じで足せたらすっきりします。add(add(a, b), c) はあまり美しくありませんね。これは

struct Point {
  double x, y;
  
  Point operator+(Point p) {
    Point sum;
    sum.x = x + p.x;
    sum.y = y + p.y;
    return sum;
  }
};

というように + 演算子オーバーロードすることで実現できます。2項演算子 + の引数が1つしかないのは,もう1つの型(1番目の引数)は自分自身であることが分かっているためです。引数として書いている Point p は2番目の引数,つまり Point 自分自身 + Point p が使われたときにこの関数が呼び出されます。

このオーバーロードというのは,型付け言語においてポリモーフィズムを実現するための仕組みの一つです。ポリモーフィズムというのは,型システムにある程度の柔軟性を持たせるために,型が多少違っても似たものであれば同じように書ければいいよね,というアイデアです。クラスベースのオブジェクト指向言語ではジェネリクス(C++ではテンプレートとも呼びます)や継承という仕組みでポリモーフィズムを実現しています*7関数型言語Haskellではジェネリクスと共に型推論という仕掛けを使って,型付けの厳格性を保ちながらも型の柔軟性も確保しようという「いいとこどり」のポリモーフィズムに挑戦しています。

演算子オーバーロードに関して,C#のインデクサとプロパティ(アクセサ)は少し興味深いものがあります。まずはインデクサから。これはC++Ruby流に言えば [ ] のオーバーロードのことです。C#はこれにインデクサ (indexer) という名前を与えました。ちょっと実例を見てみましょう。普通の配列にアクセスするには,インデックス(添字)を指定して array[3] という風に書きます。これで4番目の項目にアクセスできます。それから,最近ハッシュテーブル(Java, C#)やディクショナリ(Python, C#)という用語で普及してきたデータ構造があります。おそらく,Perlハッシュ(昔は連想配列と呼ばれていました)の貢献が大きいでしょう。これは高速にアクセスできる順序を持たない配列です。例えば phone["Smith"] で Smith さんの電話番号(文字列)を取得することができます。配列は数字のみですが,ハッシュテーブルは数字でなくても(基本的に何でも)OKです。配列は順序付き,ハッシュテーブルは順序無しのデータ構造なのですが,共通する部分を抽象化すると,「あるキーを指定すると,それに対応する値が出てくる」データ構造だと言えます。なんだか,関数のように引数を受け付けるデータ構造みたいですね。とにかく,そのようなKey-Valueペアとも呼ばれるデータ構造を定義する際に,見慣れた [ ] の記法を使うことで直感的なプログラミングが実現できるようになっています。

もう一つ,C#プロパティです。これはJavaなどでよくある getter と setter メソッドを言語のレベルでスマートに対応したもので,変数に対する参照と代入の演算子オーバーロードともいえます(変数の参照は特別な演算子がないので,演算子オーバーロードとはちょっと異なりますが)。これは,例えばオブジェクト指向におけるカプセル化(アクセス制御)の機能として「設定はできるけど読み取りはできない変数」や「読み取りはできるけど設定はできない変数」を作ったり,あるいは「最小値が0%,最大値が100%で,その範囲外の値を代入した場合は自動的にその範囲内に変更させられる変数」といった機能を実現できます。最後の例だと,そのプロパティを使う場合に object.percent = 123; などとしても,object.percent には実際に 100 が代入されるようにできます。コード例は

private double p;
public double percent {
  set {
    p = value;
    if (p > 100) { p = 100; }
    if (p < 0) { p = 0; }
  }
  get {
    return p;
  }
}

こんな感じになります。

さて,だいぶ色々と演算子オーバーロードについて見てきました。「関数は自由に好きな名前のものを定義できるけれども,演算子は自由に定義できない。けれども,既に使われてる記号で汎用的なものはオーバーロードして使うこともできる。」という例をご紹介したつもりです。

関数演算子の違いを振り返ってみましょう。関数演算子には本質的な機能差はありません。違うのは「書き方」であって,それは関数が前置記法でしか書けないのに対して,演算子は中置や後置も書けるということです。それで何がいいのかというと,よく使う演算や処理に関しては,関数のような単語を前置するのではなく,記号を中置したりした方が読みやすいということです。見た目の問題です。演算子オーバーロードについてインデクサやプロパティを含め進化を遂げているという事実は,プログラミングにとって見た目はものすごく重要なことだということでしょう。見た目の簡潔性は理解しやすさにつながります。

それでは,自由に中置記法の記号を定義して使えるような言語はないのでしょうか?ここで,最初の表に戻ります。スクロールするのも面倒だと思うので再掲しましょう。

名称 戻り値の有無記法(前置・中置・後置)
広義の関数 有り/無し 主に前置(その他,中置/後置など)
数学関数 有り 前置
演算子 有り 単項演算子は前置か後置,それ以外は中置
手続き / サブルーチン 無し 前置

最初の話で,数学関数とサブルーチンの違いは実質的には戻り値の有無でしかないから,両者の概念をあわせて「関数」として考えようと述べました。ここではさらに進んで,「関数」と演算子の違いは前置・中置・後置や使用できる記号などの記法の有無でしかないから,両者の概念をあわせて「広義の関数」として考えてみましょう。(この「広義の関数」は定着した呼び方はないと思うので,「広義の演算子」でも何でもよいでしょう。いい名前が思い浮かべば教えてください。使います。)

そのような「広義の関数」を許す,つまりプログラマが自由に中置記法や新たな記号を定義できるようにする言語が少ないのは,おそらく第一にその両者のサポートがコンパイル時の構文解析を複雑にするという問題があるからです。第二に,メリットがどれだけあるか分からないので安全策をとって言語設計者が採用しない,ということでしょう。第三に,ASCIIの範囲では使える記号は既に全て使いきってしまっているという点もあります。

しかし,実は自由にそのような「広義の関数」を定義できる言語はちゃんと存在しています。僕の知る限り,今のところただ一つで,それはPerl 6と呼ばれる言語です。例として,ある要素が配列内にあるかを判定する中置演算子 (infix operator) を一つ書いてみましょう。Perl 6ではUnicodeが使えるので,ちょっと気取って∈を演算子として使ってみましょう。

sub infix:<∈> ($element, @array) { any(@array) == $element; }

infixキーワードで∈演算子が中置記法だと宣言しています。これは if 3 ∈ @a { ... } という感じで使えます。あるいは,

sub infix:<in> ($element, @array) { any(@array) == $element; }

という風に関数名を in とすると if 3 in @a { ... } と書けます。なんか不思議な感じですね。Perl 6では中置記法以外にも,後置(postfix)や [abc] のような circumfix,それからインデクサ記法に相当する abc[x] のような postcircumfix などがあります。ほとんど使わないでしょうけどね。Perl 6ではインデクサ相当の記法(文法レベルの記法)をプログラマが定義できるという柔軟性を備えています(悪く使えば読めないコードが量産できそうです)。

そうそう,中置記法を記号で定義するだけであればHaskellでも書けます。

a +++ b = (a + b) / 2

として関数 +++ を定義すれば,1 +++ 2 と書けます。

ところで,Haskellはカッコが少ない言語として有名ですね。Haskell関数型言語なので関数の呼び出しにカッコをつけません。全ての処理が関数呼び出しによって行われる,つまり関数呼び出しが自明であるのでカッコは冗長なのですね。結合順位の明示/変更のカッコは,上のコード例でもあるように存在しますけれども。ちょっくら有名なHaskellのクイックソートを見てみましょう。

qsort []     = []
qsort (x:xs) = qsort elts_lt_x ++ [x] ++ qsort elts_greq_x
                 where
                   elts_lt_x   = [y | y <- xs, y < x]
                   elts_greq_x = [y | y <- xs, y >= x]

ここで使われている (x:xs) の ( ) は優先順位の変更を行うだけです。ちなみに [ ] はすべてリストや集合の定義ですね。ただし,関数呼び出しにカッコがないというのは,全て演算子と同様ということで,結合の優先順位や結合性を全ての関数が持ちます。結合性の宣言は

infixl 7 +++

と書きますね。それから,関数呼び出しにカッコをつけないだけでなく,引数の区切りにカンマ , を使うこともありません。なので,例えば関数とリストを引数として受け取って,リストの全要素にそれぞれ関数 f を適用した結果(リスト)を返す map 関数の適用は map f xs と書きます。map f, xs ではありません。

最後に,次のような質問を頂いたので,Perlの話で終わらせていただきます。ここでのPerlは先ほど挙げた Perl 6 ではなく現在広く普及している Perl 5 の話です。

あと、Perl の 関数で &あっても なくても文脈で判断してくれると ありますが、、あれも なんか納得いかないですねぇ 結局 どっちにすべきなんだと。。。

答え:関数呼び出しに単項前置演算子 & は使いません。

もともと,Perl 4ではビルトイン関数は & が不要,ユーザ定義関数の呼び出しには前に & をつけなければなりませんでした。しかし,Perl 5ではユーザ定義関数の呼び出しに & をつけないように変更になりました。しかし,Perl 4時代のプログラムも修正無しで動かしたいため,& をつけても関数呼び出しが行えるようになっています。しかしこれはすでに廃止された仕様なので,関数呼び出しの頭に & をつけるのは止めましょう。

ただし,関数リファレンスデリファレンスには使用します。というより,Perl 5 の & 演算子関数用のリファレンス/デリファレンスの記号 (sigil) だといえます*8関数リファレンスとはC言語でいう関数ポインタのことで,変数に「関数というデータ構造」を代入するための仕組みです。変数に「オブジェクトというデータ構造」を代入するのと似たようなものですね。このように,3 とか "string" とかといった値と同じように変数に代入したり関数引数として渡せたりするデータ構造をファーストクラスオブジェクトと呼びます(このオブジェクトオブジェクト指向オブジェクトインスタンスではなく,単なるデータという意味合い*9)。関数リファレンスデリファレンスのコード例はこんな感じです:

sub add {
  my ($a, $b) = @_;
  return $a + $b;
}

my $subref = \&add;    # リファレンス
print &$subref(1, 2);  # デリファレンス

ここで,\&add は add 関数リファレンスを取り出しています。ここに & は必須です。なければ,\add というのは,add という識別子が関数 (&add) なのか変数 ($add や @add) なのか何なのかが分かりません。Perlには変数には型がないので,$subref という変数には色々なデータを入れることができます。なので,それを「関数として」デリファレンスすることを明示しなければなりません。ちなみに配列(リスト)としてデリファレンスする場合は @$ref,スカラーとしてデリファレンスする場合は $$ref ですね。あるいは,アロー演算子を使えば関数としてのデリファレンスは $subref->(1, 2) でも OK です。ちなみに配列デリファレンスは $arrayref->[3],ハッシュデリファレンスは $hashref->{"Smith"} ですね。アロー演算子は結合の優先順位と結合性が異なるので便利です。

それから,Perlでは関数呼び出しに ( ) は不要です。標準出力には print "Hello, world!\n"; で出力できますし,同様に配列の末尾に要素を追加する場合は push @array, $element; でOKです。気に食わなければ push(@array, $element); でも構いません。基本的には「どちらの方がソースコードが読みやすくなるか?」という点でしか違いがありませんので,読みやすい方で記述します。ただし,カッコがない場合は結合の優先順位には気をつけましょう。また関数名の後に () をつけた場合,必ず「優先順位の明示/変更」ではなく「関数呼び出し」として構文解析されるので,print (3 + 5), " is 8."; なんて書くと print( (3 + 5), " is 8." ); ではなく ( print(3 + 5) ), "is 8."; として解釈されるので気をつけましょう。あやしいものにはカッコをつける。これが鉄則です。

まとめ:関数演算子の発展史は抽象化と書きやすさ・読みやすさとの取っ組み合いの歴史なのです。

*1Fortranが作られたのは昔々ですが,今でも科学技術計算関係では現役です。

*2:ただし0個の場合は関数というよりかは定数になってしまいます,逆にいえば定数は0引数関数と呼べるわけですが。関数型言語Haskellでは実際に定数はそのような扱いになっています。

*3:例えば与えられた値を2乗する「square(x) = x * x」のような関数引数xが同じであれば何度呼び出されても値は変わらないので純粋な関数です。それに対して現在の時刻を返す「current_time()」のような関数は呼び出される度に返される値が変わるので純粋な関数とはいえません。

*4:ただし,Lispでは全て前置記法で,つまり数式で「3 + 2」と書くところを「+ 3 2」のように書きます。また,Forthのように,同じ式を「3 2 +」というように後置記法(これは逆ポーランド記法とも呼ばれます)を使って書くプログラミング言語もあります。ただし,これらの言語は常に前置記法もしくは後置記法しか許さないというのが基本なので,「2項演算子を中置にしない」のではなく「演算子は全て中置にしない」といえます。

*5:正確には,一部の演算子は「副作用」を持ちます。代表的なものは代入演算子の = 演算子で,これはメモリを書き換えます。ですから,値を返さなくても意味を持ちます。とはいっても,「a = b = 3」と書けば,右結合で「a = (b = 3)」となってb = 3 が実行された結果として 3 という値が返るので,次に「a = 3」が実行され,結局 a にも b にも 3 が代入されるということができますので,メモリ書き換えの副作用も持ちますが,= 演算子自体はちゃんと値は返しております。また,この副作用を完全に排除した関数(=演算子)を純粋な関数と呼びます。

*6:ただ,単項前置の論理否定演算子 ! があるので,もしかしたら !3 はできるかもしれませんが。。。

*7オブジェクト指向における「クラス」という仕組みは,一つの静的型付け=型チェックシステムです。int x = 3; がint型のデータを表し,int型以外の値の代入を行えばコンパイルエラーを引き起こすのと同じように,MyClass c = new MyClass(); はMyClass型のデータを表しMyClassやそのサブクラス以外の値を代入しようとすればコンパイルエラーを引き起こします。C++とかJavaは完全なクラスベースのオブジェクト指向言語です。JavaScriptにはクラスが全く存在せず,プロトタイプベースのオブジェクト指向言語と呼ばれています。PerlRubyはその中間に位置する言語ですが,Perlの方がよりクラスベースからは離れているように思えます。

*8Perl変数名の最初につける $ や @ は sigil と呼びます。

*9:とは言っても,Rubyのようなまっとうなオブジェクト指向言語では関数やメソッドもオブジェクト指向的な意味合いでのオブジェクトになってますね。

suehiro3721psuehiro3721p 2007/01/30 09:39 こんにちは。
勉強になりました。ありがとうございます。
ただ,
> このオーバーロードというのは,型付け言語においてポリモーフィズムを
> 実現するための仕組みの一つです。
オーバーライドは,ポリモフィズムと関係がありますが,オーバーロードは,ポリモフィズムとは無関係ですよ。

deqdeq 2007/01/30 20:02 コメントありがとうございます。ご指摘の件ですが,たとえばWikipedia: 多重定義 http://ja.wikipedia.org/wiki/%E5%A4%9A%E9%87%8D%E5%AE%9A%E7%BE%A9
を見ても「このような型づけによる多重定義は(中略)プログラミング言語において多相型 (polymorphic type) を実現するための一つの手段であるとみなせる。」とあります。オーバーロード=多重定義がポリモーフィズムと無関係であるというご指摘の根拠は何でしょうか?
確かにポリモーフィズムを(いわゆるオブジェクト指向における)狭義の意味で「同じシグネチャを持つメソッド」や「特定のインタフェースを実装するクラスのそれらのメソッド」に限定するのであればそうともいえるのですが,関数型言語Haskellにおけるポリモーフィズムの用語の使い方から見てもそのような狭義の用法に限定されるのはあまり良い考えではないかと思います。少なくとも,そのような狭義の意味でポリモーフィズムという用語を用いていないことは本記事では明らかです。

suehiro3721psuehiro3721p 2007/01/30 22:57 オーバーロードは,一つのクラスの中で,同一メソッド名で,返値,引数の違いで実装を複数持てるという意味です。それは,ポリ(多)モ(態・相=クラス)ではないですよ。多態性というのは,一つのインスタンスがそれが所属しているクラスのインスタンスと同時に親クラスのインスタンスとして振る舞うことが出来る性質を指します。
wikipediaの多重定義の説明も間違っています。”多”がメソッドにかかっていると勘違いしてます。

deqdeq 2007/01/31 05:34 たとえばJavaというオブジェクト指向言語を一つ取って,その単一の言語のことについてだけ議論するのであれば,suehiro3721pさんのおっしゃってる定義は理に適っていることは賛成できます。それ以外にポリモーフィズムという用語を簡潔に定義できませんし,する必要がないですから。
それは前回のコメントで「限定するのであればそうともいえる」と言っている通りです(直接的にはメソッドについて語っているので不正確な印象は受けるかもしれませんが,実際は一緒のことです)。

Wikipediaのオーバーロード=多重定義の説明は間違っているとは思いません。ここではポリモーフィズムの言及は「オーバーロードは継承などと並んで多相型を実現するための一つの手段」としかありませんが,多はしっかり型にかかっておりますよ(まさかオーバーロード自体の説明に対して「多がメソッドにかかる」ことを問題視しているわけじゃないですよね?)。ここでの問題は「オーバーロードはポリモーフィズムに包含されるべき概念であるか」という点だけだと思います。それはsuehiro3721pが最初にご指摘いただいた点そのものです。
(ところで今見てみるとWikipediaの「ポリモーフィズム」の項は,定義はともかく,説明例はおかしいですね(オーバーロードでもないし,単にネームスペースの話をしている?いわゆるObjectクラスがStringValueを持っていたら確かにそれっぽいけど...)。これはひどい。)

で,そもそもsuehiro3721pさんの定義は関数型言語Haskellにおけるポリモーフィズムを説明できませんし,プロトタイプベースのオブジェクト指向言語JavaScriptにはポリモーフィズムは存在しないという結論になります。それ自体はオーバーロードとは次元での関係ない話ですが,「オーバーロードがポリモーフィズムの一種であるかどうか」を考える基本となる概念ですので,議論が必要でしょう。
それには正確な議論が必要ですから,小さいコメント欄ではなく,ぜひトラックバックでJavaやHaskellやJavaScriptにおける「ポリモーフィズムと同等とみなせる機能」を含めて話を展開していただきたいと思うのですが,いかがでしょうか?

suehiro3721psuehiro3721p 2007/01/31 07:10  おはようございます。
 先日,ちらりとみた『オブジェクト指向入門 第2版 原則・コンセプト』の巻末の用語集のポリモーフィズムの定義も私と同じでした。本書の中で,それ以外の意味づけをされて使われているかどうかまだではチェックしてません。
 多重定義やポリモーフィズムを利用して,同一メソッド名でオブジェクトごとにオブジェクトに合わせた動作させることを私は「メソッド一意性」「メッセージ一意性」と呼んでます。これとポリモーフィズムとを混乱している人が多いです。deq さんの折角の文章にもそれが現れていたので指摘させていただきました。他のオブジェクト言語に私は詳しくないので,だれかにこの件に関して書くように言っておきます。
 私のブログ等で書いたら,deq さんのところにトラックバックさせてもらいますね。

deqdeq 2007/01/31 11:03 よろしくお願いします。大多数を構成するC++/Javaプログラマにとってはその定義で問題なく平和に暮らせるのかもしれません。ですが,この問題の本質的な部分は型システム,ジェネリックプログラミング(ジェネリクス),オブジェクト指向言語(クラスベースやプロトタイプベース),関数型言語などに関して正確な知識と経験がなければ答えが得られないことだと思います。表面上の語用だけであればなんとでもなりますが。このような型システムとそれにまつわることがらに関して深い洞察をもって記述された文章があればいいのですが。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証