2008年9月15日月曜日

【CL入門】6 関数

こつこつ。写経。

  • 「ラムダ式はそれ自体はフォームではなく、したがって評価できない。」
    あれ、aclのREPLだと評価できる。。。

    CL-USER(14): (lambda (x) (* x x))
    #<Interpreted Function (unnamed) @ #x1000f1e752>

  • defunは、ラムダ式を使って関数を定義する。その際にいくつか仕事をする。

    • 自動的にラムダ式のまわりにブロックを用意する。(スコープの管理がこれでつきているのか別の仕事なのかは、ちょっとわからない。)
    • そのブロックのタグは関数名として与えられたものである。
    • その関数名をシンボル管理テーブル(のようなもの)に加える。

  • 写経なので深掘りしない。深掘りしない。。。
  • といいつつ、なんとなくわかった。上のスコープ管理のこと。defunで定義された関数名は大域であり、かつ、レキスカルスコープなので、当該関数の呼び出し時にそのときのスコープではなく、記述時のスコープになるという、スコープの途絶えが発生するということだ。(※これは間違った理解であることが、下のクロージャのところでわかることになる)
  • ここのfletやlabelsの説明は、「defunだとレキシカルスコープによってスコープの流れが変則的になること」に対して、fletやlabelsが自然なスコープの流れを維持するのに有効であるという論調。(※これも間違った理解であることが、下のクロージャのところでわかることになる)
  • おお、(funcall 'cons ...)と(funcall #'cons ...)の違いは、それが参照する環境(のシンボル管理テーブル)の違いなのか。前者は常に大域をみて、後者は常にその時点での環境階層に従う、と。(※これも中途半端な理解というか、schemer的な解釈であることが、下のクロージャのところであきらかになる。)
  • 次の例によって、#'fooが関数実体をあらわし、'fooが関数実体への参照であることがよくわかる。これはスコープの話とは基本的には別。

    CL-USER(83): (setq fdef #'foo fname 'foo)
    FOO
    CL-USER(84): (funcall fdef 10)
    11
    CL-USER(85): (funcall fname 10)
    11
    CL-USER(86): (defun foo (x) (1- x))
    FOO
    CL-USER(87): (funcall fdef 10)
    11
    CL-USER(88): (funcall fname 10)
    9

  • なお、昔から疑問があって、MLはREPL上というかtop-levelもまごうことなきレキシカルスコープなんだけど、それとくらべると、Common Lispのレキスカルスコープは中途半端な気がするのです。これはいつかちゃんと調べないといけないと思っています。

  • クロージャ。
  • 以下、あたりまえだが、CLの話。
  • evalはリスト・フォームをうけとるだけ。というのは、今自分がどこにいて(どういう状況にて)呼出されているのかという環境については何もしらない。その範囲でそのリスト・フォームを評価する、と。(例 (eval '(a b c)); evalはaを大域関数と解釈し、bとcを大域変数と解釈する。)
  • funcallについても似たようなことがある。(funcall 'a)はaというシンボルを受け取るだけで、環境についてはしらず、funcallはそのaを大域関数と解釈して実行する。
  • (funcall '(lambda (x) (* x x)) 3)についてはどうか。
  • この場合まずシステムは'(lambda (x) (* x x))を単なるリスト・フォームと解釈する。そしてそのリストをfuncallに渡す。funcallは、「リスト・フォームを受け取った場合、リストの第一要素がlamdaだった場合、それがラムダ式であるとしてxを3にバインドした上で本体を評価するという機構」を持っているのだ。(ここSchemeと違うんじゃないか、というところ。)
  • で、重要なのはこの場合もリスト・フォームしか渡してなくて、環境(情報)は渡していない、ということ。
  • なので、

    (setq y 2)
    (let ((y 4))
    (funcall '(lambda (x) (* x y)) 3))
    ; -> 6

    などとすると、funcallは自分が呼出されているところでの環境(情報)を渡されていないので、答が6になる。
  • では、

    (setq y 2)
    (let ((y 4))
    ((lambda (x) (* x y)) 3))
    ; -> 12

    はどうなのか、ということ。funcallのときは、システムはあくまでfuncallという関数呼び出しの面倒をみているだけであり、だからその関数に渡す引数を普通に(lambda式とかを特別扱いせずに)評価してあげた上で、funcallを呼出すだけだった。これは今までの説明のとおり。それとは違って、この例は、このリスト・フォームをシステムが評価するときにどういう動作をするのか?ということ。
  • で、この場合は、 ((lambda (x) (* x y)) 3)はラムダ式ではじまるリスト・フォームなので、ラムダ式が定義する関数を生成して呼出す。この生成の際に、システムは局所変数yのスコープにいることを尊重して(知っている)ということがCLの仕様として決っている、ということ。
  • ここでちょっと考える。CLではやはりlambda式はクロージャをつくらないのか、ということ。よりイメージを正確にいうと、lambda式の評価結果がクロージャではないのか、ということかな。(追記:いや、正確に言うとCLではラムダ・フォームというのは存在せず、そういう意味においての評価はされない(できない)ので、これは阿呆な言明である。)
  • 上の議論は、クロージャをつくらない、ということだと思う。
  • なので、ラムダ式というのは、頭がlambdaのリスト・フォームにすぎなくて、それを扱う側が特別視しないかぎり、単なるリストということかな。特別視されるのは、リスト・フォームの頭がラムダ式のときやfuncallの第一引数に与えられたときなど。

  • で、クロージャをつくるのはfunctionだと。略記は"#'"。
  • なので、先の例は、quoteじゃなくてfunctionにすれば、環境情報までfuncallにわたる

    (setq y 2)
    (let ((y 4))
    (funcall #'(lambda (x) (* x y)) 3))
    ; -> 12

  • すなわち、スペシャル・オペレータたるfunctionは、それが呼出された場所の環境情報を集めて、それらをラムダ式に付加したデータを生成する(このデータ(やその型)をクロージャと呼ぶ)。(Functionにすぎないfuncallやapplyは環境をいじれないが、Special operatorであるfunctionは環境をいじれる。ややこしい。。。)
  • funcallやapplyは、引数がクロージャであるなら、それが持つラムダ式と環境情報とを利用して処理を進める。
  • クロージャは数値などと同じ意味でのデータなので、名前をつけて参照したり、いろいろ受け渡したりできる。(う、説明が面倒になってしまい。。。ファーストクラスということかと)

  • この本、CLのクロージャの説明で、今までで一番わかりやすかったような。
  • でも、他のものをいろいろ見た後で見ているからこの本のものが初見でわかりやすいかどうかはわからない。

  • 思うところ。正確には表現できないのだが、たぶん、Schemeでは処理系の内部の環境モデルとクロージャというものと、言語の外部仕様というか形式としてのlambdaがもつ意味とが、よりダイレクトに対応しているのではないか。CLでは、言語の内部ではfunctionが無いフォームの処理においても環境モデルやクロージャを使っている(処理系依存というか処理系の作者の判断だが)としても、言語を使う側としてのクロージャというもの(処理系内部のクロージャと同一仕様というか同じものとは限らない)を利用する場合は明示的にfunctionというスペシャル・オペレータを使わなければいけない、ということじゃないかなぁ。(長い。。。)
  • なお、ここらへんは、処理系内部に関する知識がない現状の私では、間違えている可能性もある。私はよく間違えるし。
  • 早くコンパイラの勉強をして、自分でこれが正しいのか違うのか判断できるようになりたいなぁ。
  • お、これやってみたらおもしろい。

    CL-USER(108): (lambda (x) (* x y))
    #<Interpreted Function (unnamed) @ #x1000b40dd2>

    CL-USER(109): '(lambda (x) (* x y))
    (LAMBDA (X) (* X Y))

    CL-USER(110): #'(lambda (x) (* x y))
    #<Interpreted Function (unnamed) @ #x1000b4d732>

    CL-USER(111): (let ((y 4))
    #'(lambda (x) (* x y)))
    #<Interpreted Closure (unnamed) @ #x1000bb8aa2>

    上で説明したことや、top-levelの特殊性についてよくわかる。

写経だけのつもりが、、、
でも楽しめたので、いいや。

# ここに書いた説明は、Common Lisp 入門の内容そのまんまではなくて、私の理解で書いたところがそこそこあります。なので、間違いがあったとしてもそれはおそらく私が原因です。

0 件のコメント: