読者です 読者をやめる 読者になる 読者になる

Snippets

対抗して作ってみた。

さらばいとしの lexical-let

どうもこんにちは。dageziです。闇アドベントカレンダー参加エントリーです。嘘です。本当は .Emacs advent calendar参加エントリーです。なんか向こうが楽しそうなので羨ましくなって絡んでみたくなっただけです。

でも一応 emacsの闇っぽい部分について書いてみたいと思います。参加している他の皆さんのように役に立つことを書いてみようかとも思ったのですが、そんなにキラキラしたものより、役に立つかわからない emacsの重箱の隅をつついてみるのも いいかなあ、と思ったので。普段そんな記事を書いても見向きもされないだろうし、アドベントカレンダーですこしぐらい流入があるすきに書かせてください。

Lexical-scope がすき!

Emacs lispは、当然 Lispであるので、一級の関数をばりばりつかえます。mapcarとか add-hookとかでみなさんつかっているでしょう。 とはいうものの、最近の言語では当然である lexical-scopeはありませんでした。そのため、関数型言語としての表現力は一段下でした。

しかし Emacs 24で事情は変わりました!lexical-bindingsがちゃんと言語機能として入ったのです!!

でもこのエントリー、そういうまっとうな機能の方は扱いません。そちらはそちらで突っ込むと面白いのですが、今回はよりマイナーに、Version 23以前からあった CLパッケージ内の lexical-let機能について突っ込んでみようと思います。

lexical-letとは

lexical-letマクロは、先程述べたように lexical scopeのなかった emacs 23以前でがんばって lexical-scopeを実現するための仕組みです。実際にその動作をみてみましょう。

(defun make-counter ()
  (lexical-let ((x 0))
    (lambda () (setq x (1+ x))))

よくある counterをつくる関数です。以下のように *scratch* などで実行してみれば正しく動作して、counter1と counter2は別々の xを参照してばらばらに動作していることがわかります:

(fset 'counter1 (make-counter))
(fset 'counter2 (make-counter))

(counter1) 
 => 1
(counter1) 
 => 2
(counter2) 
 => 1

ちなみに、lexicalじゃない普通の dynamicな letを使った場合、(coutner1)を実行した時にエラーになると思います。あしからず。

では早速なかみを見てみます (pp (symbol-function 'counter1)) すると以下のとおりに:

(lambda
  (&rest --cl-rest--)
  (apply
   '(lambda
      (G51610)
      (let*
          ((v G51610))
        (set v
             (1+
              (symbol-value G51610)))))
   '--x-- --cl-rest--))

へんな実装に変わってしまってます。シンボル --x-- に lambda式を適用してごにょごにょしてます。 --x--を直接扱ったほうが早いだろうに。それにこれじゃ --x--は globalのものを見ることになります。どうやって counter1 と coutner2の区別をつけるんだろう。と思って覗いてみると:

--x-- 
error -> Symbol's value as variable is void: --x--

エラーになってしまいました。でも counter1関数は正しく動いてるのです。なんででしょう?

わざとらしかったけど、種明かしは以下のとおりです。

(cadadr (cdaddr (symbol-function 'counter1))) 
  =>  --x--
(eq (cadadr (cdaddr (symbol-function 'counter1))) '--x--)
  => nil

このように counter1 の定義にあるシンボル--x-- と普通に書いたシンボル --x-- は別物なのです。実は、前者は make-symbolを使って作った、internされてないシンボルでした。 internされてないので readerはそれを見つけることができず、別の --x--になってしまうのです。

つまり普通はどうやっても lexical-letで導入された --x-- にさわることは外側からはできません。グローバルスコープなのに触ることもできない、そんな隔靴掻痒な変数でクロージャを作ってしまうとは!これを知った時にはあっけにとられたものでした。

ちなみに、普通では触れない closure内部の --x--を強引に触ることもできます。そう、さきほどやったように:

(set (cadadr (cdaddr (symbol-function 'counter1))) 100)
 => 100
(counter1)
 => 101

シンボルって奥が深いですね。

でも無駄遣いじゃない?

実は、この実装を知った時に気になったのはメモリの無駄遣いにならないか、とういうことでした。普通のグローバル変数は永遠の寿命を持ちます。そういうわけで、この internされてないシンボルも永遠の寿命を持ち、クロージャが参照されなくなってもメモリを圧迫し続けるんじゃないかと。gkbr

まあそういうときは実際に調べてみましょう。ちょうど、garbage-collectの戻り値を見れば生きているシンボルの数もわかるようです。 (assq 'symbols (garbage-collect))で帰ってくるリストの3番目の要素が使われてるシンボルの数です。

ということで、一万個ゴミシンボルを作ってテストしてみました:

(assq 'symbols (garbage-collect))
=> (symbols 48 45592 46)

(let ((i 0)) (while (< i 10000) (set (make-symbol (concat "aho" (number-to-string i))) i) (incf i)))
=> nil
(assq 'symbols (garbage-collect))
=> (symbols 48 45592 46)

めでたく、使われてるシンボルの数が増えてないことが確認できました。よかったよかった。

このように Hackyで素敵な lexical-letですが、今後は活躍の場がなくなると思うと悲しいです。ということで、次回は emacs24の lexical-scopeを disる記事でも書きます。うそですが。