教えて!gooで面白い質問があがっていた。
プログラミングで変数と関数の違いはなんでしょうか?pythonで独学しています。専門用語を使わずに教えていただけましたら理解ができます。
よろしくお願いします。
みんななら何て答えるだろうか。
いや、素人臭い質問だ、とか一笑に付してはいけない。
これはこれでかなり含蓄がある質問だと思う。
もちろん、「専門用語を使わずに」と言われれば説明しようがないわけだが。
ここにある思考プロセスは関数と変数が対立概念である筈、と言う前提がある。
それはある意味正しいのだが、反面間違っているだろう。
ハッキリ言うと、「何かを定義する」と言う意味に於いては「プログラミング言語によって違う」というのが一つの解になる。
ある言語では「関数定義」と「変数定義」は全く違う概念だしプロセスだ。
どういう言語が原理的にそうなのか。それは「関数がファーストクラスオブジェクトではない言語に於いて」だ。
例えばC言語だと関数定義法と変数定義法は別じゃないとならない。
この二つは様式に於いて異なり、それは意味合いが違う、と言う事を表している。
では「関数がファーストクラスオブジェクトな言語」ではどうだろう。
極論すると、一般的には「関数定義」と「変数定義」は全く同じである。実はこの二つは差がない。と言うか差があるべきではない、ってのが一つの答えだ。
我々はそういう言語を既に知っている。Schemeだ。
僕自身、初めてSchemeを学んだ時、ビックリしたクチである。
「関数定義と変数定義が同じな言語なんて!凄い!」
と。
そう、Schemeには実は「関数定義」と言う概念がない。あるのは「関数オブジェクト」の定義である。
「関数オブジェクトの定義」と聞くと厳しく思えるが、データ型の定義法自体はそのデータ型によって違う、ってのが当たり前なわけで、実は関数オブジェクトを定義する、と言うのは考え方としては配列オブジェクトを定義する、とかリストを定義する、とかハッシュテーブルを定義する、と言うのと何も変わらない。
要するにオブジェクトはその様式に従って生成法は違うが、ある意味「扱い」自体はどれも同じに扱える、と言う事だ。
つまり、それらを変数に代入する、と言う事に於いては全く差がない、と言うのに感動したわけである。
> (define x 1) ;; x に整数オブジェクトを代入
> (define y '(1 2)) ;; y にリストオブジェクトを代入
> (define z (vector 1 2)) ;; z に配列オブジェクトを代入
これらは全部オブジェクトを変数に代入してる。つまり変数を定義してるわけだ。
全く同じ様式として関数オブジェクトを変数に代入可能なのがSchemeである。
> (define w (lambda (i) (+ i 1))) ;; w に関数オブジェクトを代入
Schemeで関数オブジェクトを作る為のコマンドが要するにlambdaである。
ではwは関数なのだろうか、それとも変数なのだろうか。
当然「どちらでもある」が、表層的には実は変数だ、と言う事が分かるだろう。上のwはまさしく「変数wを定義した」だけであり、その中身が関数オブジェクトなのかどうかはwを実行しないと分からない。
でも、それはどの変数でも「実行しないと分からない」と言うのは実は同じなのである。実行してみて「どれだ」と言うのが分かるのであり、プログラマ側が忘れてたとしたら、「オブジェクトを代入した変数が何なのか」は基本的にはサッパリ分からんのである。
> x
1
> y
'(1 2)
> z
'#(1 2)
> w
#<procedure:w>
>
Schemeが示した優れたメッセージは、
関数オブジェクトが存在し、名前空間が一つしかないのなら、関数定義は必要ない
という事なのである。
これは凄い事なのである。これを示してくれたSchemeに、学習当初、物凄く感動したわけである。
さて、そう考えると、プログラミング言語によっては、原理的には別に変数定義と関数定義は対立する必要がない、と言うことが分かるだろう。
変数はそのプログラミング言語で定義されているオブジェクト、言い方を変えるとデータ型だったら何でも代入可能だし、関数がファーストクラスオブジェクトだ、と言う文が指し示す事は関数でさえそれらデータ型の仲間である、と言う事だ。
敢えて対立構造を持ち出すのなら変数とデータ型全てが対立してるのであり、関数がデータ型である以上、別のデータ型と違う、と言う必要がないのだ(※)。
じゃあ、ANSI Common Lispはどうなのか。
ANSI Common Lispは関数オブジェクトが存在するにも関わらず関数定義と変数定義が分かれている。
それをもってSchemeに劣ってる、と言えるのだろうか。
それは違う。
平たく言うとANSI Common LispとSchemeでは名前空間の数が違うのだ。
Schemeは名前空間が一つのLisp-1、ANSI Common Lispは名前空間が二つ以上あるLisp-2。
もっと言えば、ANSI Common Lispは最低でも、変数用の名前空間と関数用の名前空間が分かれている。従ってどうしても変数定義≠関数定義なのだ。
CL-USER> (defun foo(x) (1+ x))
FOO
CL-USER> (defparameter foo 1)
FOO
CL-USER> foo
1
CL-USER> (symbol-function 'foo)
#<FUNCTION FOO>
CL-USER> (symbol-value 'foo)
1
CL-USER>
故に、ANSI Common Lispが示唆してるメッセージは次のようなモノとなる。
関数オブジェクトが存在するプログラミング言語の変数用の名前空間と関数用の名前空間が分かれている時は変数定義と関数定義は別でないとならない
逆に言えば、Cみたいにビルトインのデータ型が貧弱で関数オブジェクトを持たない言語ならいざ知らず、関数オブジェクトを持つ殆どの名前空間が一つしか無い言語では、理論的には関数定義は必要ない、と言う事だ。
これはある意味驚くべきメッセージじゃないだろうか。
ただし理論的には、である。
実際は最適化等に関わる実装上の理由で、関数定義と変数定義が違わなければならない、と言った事もあるだろう。
じゃなければC言語脳的な保守思想で、プログラミングを書く層が不便を強いられてる、って事になる・・・・・・いや、ホント実は不便なんだよ、気づきにくいけど。
JavaScriptの場合は関数定義と関数式(いわゆるラムダ式)はSchemeに倣い殆ど同じである。
// 関数定義function foo(x) {
return x + 1;
}// 変数にラムダ式を代入let bar = function (x) {
return x + 1;
}
ただし、機能が若干違って、それは関数定義の前にその関数名を使って何かやりたい、と言った場合O.K.だけど、ラムダ式の場合は不可能だ、と言う辺りだ。
それをJavaScriptでは関数宣言の巻き上げ、と呼んでる模様。
ただし、それ以外では特に差がない辺りが、さすがCの皮を被ったLispである。
HaskellはよりScheme方式で関数定義と変数へのラムダ式代入は全く同じ効果の模様だ。やったね!
foo x = x + 1
bar = \x -> x + 1
残念なのはPythonである。
Pythonは関数オブジェクトを持ってるし、ラムダ式もあり、Lisp-1だが、関数定義defはラムダ式を用いた変数への代入の構文糖衣ではない。
つまり、Schemeの示した理論に今一歩及んでいないのだ。
そもそもラムダ式がヘナチョコだし、Javaのラムダ式と似たような中途半端さだ。方向性は違うけどね(例えばJavaのラムダ式はJavaScriptと違ってそのままreturn出来ない)。
まぁ、中庸言語Pythonだからなぁ。機能が研ぎ澄まされてないのはしょーがないのかもしれない。
と、実は「全てがオブジェクト」で、Lisp-1と言う縛りだと関数定義は実は必要なく、言い換えると関数定義と変数定義には差を持たせる必要がないんだ、と言う事がお分かり頂けただろうか。
Schemeが指し示した世界では関数と変数の境界線は、「定義する」と言う作用に於いては、実は凄く曖昧なのである。
※: ちなみに、Schemeの仕様書にマトモに従った場合。
例えばSchemeインタプリタを使って、
> (define w (lambda (i) (+ i 1)))
と「関数を定義した」はいいが、何かの事情によってwを再定義せなアカン、となった場合。
本来は続けてインタプリタに入力するのは次のようにして行うのが正しい。
> (set! w (lambda (i) (+ i 2)))
そう、defineで定義された「変数」はset!で束縛し直すのが本来なら正しいやり方、って事になる。
- define -> 変数定義はひとつ以上の識別子を束縛し、その初期値を指定
します。
つまり、defineは元々はあくまで初期値設定の為の構文なのである。
ただし、トップレベルではもちろん
> (define w (lambda (i) (+ i 3)))
とdefineで再定義しても構わない、と保証されてはいるが、問題はローカル関数だった場合、である。
まぁ、こんなバカなコードは書く必要がないが、一応例示してみよう。
(define foo
(lambda (x)
(define bar
(lambda ()
(+ x 1)))
(define bar
(lambda ()
(+ x 2)))
bar))
関数fooの中でローカル関数barを定義してるが、そのままbarをdefineで再定義しようとしてる。
トップレベルだと問題がないが、ローカル環境(つまりトップレベルではない)だとエラーになる。
このバカなコードを実行させるには2番目のbarはset!せなアカン、と言う事だ。
(define foo
(lambda (x)
(define bar
(lambda ()
(+ x 1)))
(set! bar ;; こうすれば上で定義されたbarを再定義可能
(lambda ()
(+ x 2)))
bar))
こうすれば問題なく、ローカル環境での再定義が可能となり、最初に定義されたbarの中身は消えて無くなってしまう。
もちろんこんなバカなコードは書く必要はない。
ないがdefineとset!の役割の違い、ってのはアタマに入れてていいだろう。