星田さんの記事に対するコメント。
えっ! ()の数が関数内で合わなくても大丈夫なのか・・
いやいや、当然カッコとコッカの数の帳尻は合ってないとなんない。
ちと紛らわしい書き方だったかな・・・。
つまり、例えばこういう連想リストがあるとするでしょ?
(define table `((k . ,(+ 1 2))))
まぁ、例なんで、1要素しかない連想リストなんだけど。
ここからキーがkである値を取り出すとする。
> (cdr (assq 'k table))
3
>
まぁ、当然こうなるよね。
ところが値がラムダ式で包まれていた場合。
(define table `((k . ,(lambda () (+ 1 2)))))
上と同じようにすると単にクロージャが返ってきちゃう。
> (cdr (assq 'k table))
#<procedure>
>
これを値を「実行させる」ようにしたい。クロージャが返ってくる以上関数として実行させないとダメだ、ってな話だ。
当然、こうなる。
> ((cdr (assq 'k table)))
3
>
カッコとコッカが多いでしょ?これはassqで取り出した値がクロージャだからだね。結局無引数関数を実行させるのと同じになる。
> ((lambda () (+ 1 2)))
3
>
これ、Pythonなんかの方が見た目分かりやすいかもしんない。
例えば変数にラムダ式を代入して、
>>> v = lambda : 1 + 2
>>>
とかすると変数vは「実行可能」になる。
>>> v
<function <lambda> at 0x7f9ec9774160>
>>> v()
3
>>>
v自体は当然クロージャが代入されてるんだけど、カッコを付けて呼び出せば当然関数と同様に「実行される」。
それだけ、の話なの。ただ、Lispの場合「外側にカッコとコッカが増える」んで、見た目ピンとこねぇよな、と言う話。
しかしLambdaはミステリアスだなぁ
つまりこーゆー事だ。
アントニオ・ラムダ談(謎
一応、Lisp自体はラムダ算法をヒントに誕生してるんだけど、「なんでもラムダ」って言い始めたのはそれこそSchemeが登場してから、じゃないかな。
こないだやってた「継続受け渡し」(CPS)を通じてレキシカル・スコープ込みのラムダ式ってのが強力だ、ってのが論文で発表されてからラムダ・ラムダ言い出したと思う(事実、旧いスタイルのEmacs Lispではそこまでラムダラムダ言ってない)。
例えば実用Common Lisp(PAIP)の412ページ辺りを見て欲しいんだけど。ここでは「ラムダ式」でオブジェクト指向での「クラス」がどのように実装されていくのかが書かれている(※1)。
Racketなんかで書けば次のようなカンジかな。
(define (new-account name (balance 0.00) (interest-rate .06))
(lambda (message)
(case message
((withdraw) (lambda (amt)
(if (> amt balance)
'insufficient-funds
(begin (set! balance (- balance amt))
balance))))
((deposit) (lambda (amt)
(set! balance (+ balance amt))
balance))
((balance) (lambda () balance))
((name) (lambda () name))
((interest) (lambda ()
(set! balance
(* interest-rate balance))
balance)))))
クラスでオブジェクトを作成するような事はLispではラムダ式で出来る。
そしてこれは同時に「メソッド」が何なのか、の理論的な定義だ(※2)。
> (define acct (new-account "星田" 10000000)) ;;; いわゆるインスタンス生成
> acct
#<procedure>
> ((acct 'withdraw) 5000000) ;;; withdraw メソッド
5000000
> ((acct 'deposit) 1234500) ;;; deposit メソッド
6234500
> ((acct 'name)) ;;; インスタンス変数 "name" の確認
"星田"
> ((acct 'balance)) ;;; インスタンス変数 "balance" の確認
6234500
>
もちろん実用的な意味では、ラムダ式でわざわざ「クラス」を組み立てるのは無駄だと思う。
ただし、何故に大学なんかの研究機関で長年Lispが愛用されてきたのか、その一旦が分かるんじゃないか。オブジェクト指向はオブジェクト指向言語でしか通常扱えない。
ただし、Lispならラムダ式のパワーを使って「新しいアイディアを実装/検証する」事が可能だ、と言う事だ。他の言語だとその言語の基礎機能として組み込まなきゃならないものがLispでは「理論検証」用に「Lispの上で」実装する事が出来る。
ちなみに、上の例はcaseでの実装だけど、当然連想リストを使って同じモノを実装する事が出来る(※3)。
(define (new-account name (balance 0.00) (interest-rate .06))
(let ((table `((withdraw . ,(lambda (amt)
(if (> amt balance)
'insufficient-fund
(begin (set! balance (- balance amt))
balance))))
(deposit . ,(lambda (amt) (set! balance (+ balance amt))
balance))
(balance . ,(lambda () balance))
(name . ,(lambda () name))
(interest . ,(lambda ()
(set! balance
(* interest-rate balance))
balance)))))
(lambda (message) (cdr (assq message table)))))
※1: オブジェクト指向の初心者向けの例として、良く使われるのが「銀行口座」の例と「図形の面積」の例だ。
なお、ラムダ式を使った同様の例はSICPでも扱われている。
※2: オブジェクト指向の残り半分は「継承」だと一般には思われている。それはここでは実装出来ない。
しかし、「インスタンス作成の仕組み」と「メソッド」と言う大きな2つに関してはラムダ式で理論的には簡単に組み立てられるのだ。
※3: なお、ここで挙げたコード例はPythonのオブジェクト指向で言うと、
class new_account(object):
def __init__(self, name, balance = 0.00, interest_rate = .06):
self.name = name
self.balance = balance
self.interest_rate = interest_rate
def withdraw(self, amt):
if amt > self.balance:
raise ValueError("insufficient fund")
else:
self.balance -= amt
return self.balance
def deposit(self, amt):
self.balance += amt
return self.balance
def interest(self):
self.balance *= interest-rate
return self.balance
にあたる。ほぼやってる/やれる事は同じだ。
>>> acct = new_account("星田", 10000000)
>>> acct
<__main__.new_account object at 0x7fc5724c5d30>
>>> acct.withdraw(5000000)
5000000
>>> acct.deposit(1234500)
6234500
>>> acct.name
'星田'
>>> acct.balance
6234500
>>>
違いは、
- Scheme版はインスタンス変数にアクセスするのもメソッドに頼らざるを得ない(Javaで言うgetter/setter的?)
- Scheme版には継承機能がない
程度だ(もっともそれはそれでデカいが)。
しかしながらいずれにせよ、世の「オブジェクト指向」が何を意図してどう設計されているのか、と言うのはSchemeのラムダ式経由で眺めてみると意外と分かりやすい筈だ。