#:g1: frontpage

 

実践SETF定義:define-modify-macroで頻出パターンをまとめる

Posted 2018-12-13 14:28:27 GMT

Lisp SETF Advent Calendar 2018 13日目 》

setf系構文の紹介ですが、今回は、define-modify-macroの紹介です。

define-modify-macroは、簡単に説明するなら、(setq x (fn x))のようなパターンを簡潔に(fnf x)と書けるようにする便利定義構文なのですが、このようなパターンが頻出する訳でもないので、普段はそんなに使うこともない印象があります。

define-modify-macroで何か定義してみる

(setq x (cdr x))

というパターンはそこそこ使いますが、define-modify-macrocdrfでも定義してみましょう。

(define-modify-macro cdrf () cdr)

(let ((x (list 0 1 2))) (cdrf x) x)(1 2)

まあ、実質同じことをするビルトインマクロにpopがありますが……。
一応返り値が違います。

定義構文の注意点として、代入にされる場所となる変数が先頭に来て、さらにそれが定義構文中には現われない、という点です。
cdrの場合は1引数なので、0引数で記述します。

define-modify-macroを使えば役に立ちそうな立たなそうな構文を大量生産できます。

(define-modify-macro listf (&rest args) list)

(let ((x (list 0 1 2))) (list x 1 2))((0 1 2) 2 3 4)

(define-modify-macro =f (&rest args) =)

(let ((x 0) (y 1)) (=f x y ) (list x y))(nil 1)

appendfnconcf等々は定番アイテムで、色々なユーティリティライブラリに含まれています。
まあ、ライブラリを読み込むのが面倒な場合には、簡単なので定義してしまっても良いでしょう。

modify-macrolet

define-modify-macroは大域定義になるのですが、7年程前にローカル構文版を思い付いて作ったことがありました。

これを使うと、

(modify-macrolet ((nconcf (&rest args) nconc))
  (let ((x nil))
    (nconcf x (list 0 1 2 3))
    x))

のように大域の名前を汚染することなくローカルに書くことが可能になります。

ちなみに、個人的には定義してから7年間一度も使ったことはないですが、ご興味のある方は如何でしょうか。
また、modify-macroletを自作してみるのも一興かなと思います。


HTML generated by 3bmd in LispWorks 7.0.0

Flavorsのメソッドコンビネーションを眺めたり再現してみよう: :pass-on

Posted 2018-12-12 19:45:24 GMT

Lisp メソッドコンビネーション Advent Calendar 2018 13日目 》

前回に引き続き、メソッドコンビネーション元祖のFlavorsに標準装備されていたメソッドコンビネーションを眺めたり再現してみたりしようと思います。

メソッドの返り値を順繰りに次のメソッドに送っていく:pass-onの再現がしたくて苦戦していましたが、なんとか形にできました。

Flavorsの:pass-onの挙動を確認してみる

とりあえず、LMI Lambdaのエミュレータで挙動が確認できたのですが、こんな感じでした。

(defflavor a () ()
  (:method-combination (:pass-on (:base-flavor-last) :m)))

(defflavor b () (a))

(defmethod (a :m) (x y) (values (list :a x) (1+ y)))

(defmethod (b :m) (x y) (values (list :b x) y))

(send (make-instance 'b) :m 0 1)(:a (:b 0)) 2

aを継承するbという2つのflavorに、それぞれm:pass-onで定義し、bのインスタンスでmを呼び出すと、b.mの返り値をa.mが受け取るようになります。
:base-flavor-lastなのでbaの順番ですが、:base-flavor-firstにすれば、逆にもできます。

:pass-onが多値で返す意味が分からなかったのですが、良く考えると、メソッドの引数が複数になる場合は、多値かリストにして対応するしかなく、リストだと受取側の引数のインターフェイスを変更しないといけないので、多値で返すしかないですね。なるほど。

Common Lispで再現してみる

MOPのmake-method-lambdacompute-effective-methodあたりでどうにかできないか検討しましたが、call-methodフォームを作成して、また分解して、という感じになってしまうので、call-methodをスルーして、メソッドの関数をmethod-functionで取り出して直接呼ぶことにしました。

(ql:quickload :closer-mop)

;; LispWorks/Allegro CL (define-method-combination :pass-on () ((ms ())) (:arguments &rest args) (let ((vs (gensym "vars-"))) `(let* ((,vs ,args) ,@(loop :for m :in ms :collect `(,vs (multiple-value-list (apply ,(c2mop:method-function m) ,vs))))) (declare (dynamic-extent ,vs)) (values-list ,vs))))

しかし、メソッドの引数情報が欲しいので、define-method-combination:argumentsを指定しているのですが、SBCLだと:argumentsがちゃんと実装されていないようなので&restが使えません。

また、method-functionが返す関数の引数は、argsnext-methodsの筈なので、(funcall method-function vs nil)とするのが正しそうですが、LispWorksとAllegro CLでは、(apply method-function vs)でないと上手く動かない謎。

そんなこんなで、まともに動くものができているとは言い難いですが、こんな感じに書けます。

(defclass a () ())

(defclass b (a) ())

(defgeneric m (o x y) (:method-combination :pass-on))

(defmethod m ((o a) x y) (values o (list :a x) (1+ y)))

(defmethod m ((o b) x y) (values o (list :b x) y))

(m (make-instance 'b) 0 1) → #<b 40205BA353> (:a (:b 0)) 2

メソッドコンビネーションの展開はこんな感じで別段変なことはしていませんが、どの処理系も何かしらおかしい感じです。
まあ、LispWorksとAllegro CLで動くので良しとしましょう。

(mc-expand #'m :pass-on nil (make-instance 'b) 0 1)(let* ((#:|vars-131621| args)
       (#:|vars-131621|
        (multiple-value-list (apply #<Function (method m (b t t)) 4140032074>
                                    #:|vars-131621|)))
       (#:|vars-131621|
        (multiple-value-list (apply #<Function (method m (a t t)) 41400320FC>
                                    #:|vars-131621|))))
  (declare (dynamic-extent #:|vars-131621|))
  (values-list #:|vars-131621|)) 

まとめ

HyperSpecによると、define-method-combination:argumentsでは、&restの他に&wholeも使えたりするみたいですが、メジャーな処理系を試してみたところ、まともに:argumentsの機能を実装しているものは無いように思えます。

SBCLは、ソースを眺める限り&restを処理できていないのですが、今後修正されれば、今回の:pass-onも動くんじゃないかなと思います。


HTML generated by 3bmd in LispWorks 7.0.0

Flavorsのメソッドコンビネーションを眺めたり再現してみよう: :daemon-with-override

Posted 2018-12-12 13:48:13 GMT

Lisp メソッドコンビネーション Advent Calendar 2018 12日目 》

前回に引き続き、メソッドコンビネーション元祖のFlavorsに標準装備されていたメソッドコンビネーションを眺めたり再現してみたりしようと思います。

本当は、:pass-onの再現がしたいのですが、色々間に合ってないので、:daemon-with-overrideの再現でお茶を濁したいと思います。

:daemon-with-overrideは、Common Lispでいうと、:aroundなしのstandardメソッドコンビネーションの前に門番のメソッドがいるような感じですが、こんな感じに定義できます。

(define-method-combination :daemon-with-override ()
  ((before (:before))
   (after (:after))
   (primary ())
   (override (:override)))
  `(or 
    ,@(and override `((call-method ,(car override))))
    (multiple-value-prog1 
        (progn
          ,@(loop :for m :in before :collect `(call-method ,m))
          (call-method ,(car primary) ,(cdr primary)))
      ,@(loop :for m :in (reverse after) :collect `(call-method ,m)))))

細かいオプションは詰めてないですが、これくらいの定義なら空で書けるようになりました。
難解だと思っていたdefine-method-combination構文ですが、10個位定義を書けば、そこそこ覚えられそうですね。

では、使ってみましょう。

(defgeneric foo (x)
  (:method-combination :daemon-with-override))

(defmethod foo ((x T)) x)

(defmethod foo :before ((x T)) (print :before))

(defmethod foo :after ((x T)) (print :after))

(defmethod foo ((x rational)) `(rational ,(call-next-method)))

(defmethod foo ((x integer)) `(integer ,(call-next-method)))

(defmethod foo :override ((x number)) (evenp x))

(foo 8)
→ t 

(foo 9) ▻ :before ▻ :after → (integer (rational 9))

メソッドコンビネーションを展開してみるとこんな感じになります。

(mc-expand #'foo
           :daemon-with-override
           nil
           1)(or (call-method #<standard-method foo (:override) (number) 40201044DB>)
    (multiple-value-prog1
        (progn
          (call-method #<standard-method foo (:before) (t) 402045306B>)
          (call-method
           #<standard-method foo nil (integer) 4130468943>
           (#<standard-method foo nil (rational) 413046892B>
            #<standard-method foo nil (t) 413046961B>)))
      (call-method #<standard-method foo (:after) (t) 4020462243>)))

まとめ

:daemonメソッドコンビネーションに門番がついているというパターンなので、Common Lispでは、:aroundを使えば実現できるパターンですね。

数あるパターンが、Common Lispでは、:aroundとして集約されたという流れが想像できます。


HTML generated by 3bmd in LispWorks 7.0.0

実践SETF定義: setf placeって多値が取れたり取れなかったりする?

Posted 2018-12-12 13:04:44 GMT

Lisp SETF Advent Calendar 2018 12日目 》

前回の(setf all)を定義していて気付いたのですが、incfや、pushpopvaluesと組み合わせると処理系によって異なる動作をします。

下記のような例は、SBCLやAllegro CLでは中途半端な動きをしますが、LispWorksではマクロ展開時にエラーになります。

(let ((x 0)
      (y 0))
  (incf (values x y))
  (list x y))(1 nil) or error

(let ((x (list 0 1 2)) (y (list 1 2 3))) (pop (values x y)) (list x y))((1 2) nil) or error

もしや、setf以外はvaluesを取れないのかと思い、HyperSpecを確認してみましたが、5.1.2.3 VALUES Forms as Placesでもvaluessetf以外では機能しない、とは書いてありません。

間接的な定義がされている場合もあるので、それらしきものを探してみましたが、setfベースのマクロについての定義があります。

decfpoppushnewincfpushremfdefine-modify-macroで定義されているような挙動をする的な解釈が成立しそうな雰囲気もありますが、 define-modify-macroのAPIからすると、関数の引数として値を処理するので、多値は扱えません。
define-modify-macroで定義すれば、incfのようなものは、

(let ((x 0)
      (y 0))
  (setf (values x y)
        (values (1+ (values x y))))
  (list x y))(1 nil) 

のように展開されると思われるので、中途半端な返り値になっている理由も合点はいきます。

多値を扱いたい

とりあえず、多値を扱えるフォームはどんな感じになるか定義して眺めてみます。

incfの多値版は、こんな感じに定義できるかなと思います。

(defmacro incf* (place &optional (delta 1) &environment env)
  (multiple-value-bind (dummies vals newval setter getter)
                       (get-setf-expansion place env)
    (declare (ignore dummies vals setter))
    (let ((deltas (loop :for () :on newval :collect (gensym))))
      `(multiple-value-bind ,newval
                            ,getter
         (setf (values ,@deltas) 
               ,(if (eql 1 delta)
                    `(values ,@(loop :for () :on deltas :collect 1))
                    delta))
         (setf ,getter
               (values ,@(mapcar (lambda (v d)
                                   `(incf ,v ,d))
                                 newval
                                 deltas)))))))

(let ((x 0) (y 1)) (incf* (values x y) (values 1 10)) (list x y))(1 11)

popの多値版はこんな感じ

(defmacro pop* (place &environment env)
  (multiple-value-bind (dummies vals newval setter getter)
                       (get-setf-expansion place env)
    (declare (ignore dummies vals setter))
    (let ((retvars (loop :for () :on newval :collect (gensym))))
      `(multiple-value-bind ,newval
                            ,getter
         (let (,@(mapcar (lambda (a d)
                           `(,a (car ,d)))
                         retvars
                         newval))
           (setf ,getter
                 (values ,@(mapcar (lambda (v)
                                     `(cdr ,v))
                                   newval)))
           (values ,@retvars))))))

(let ((x (list 0 1 2)) (y (list 0 1 2))) (pop* (values x y)) (list x y))((1 2) (1 2))

VALUES Forms as Placesを処理できるdefine-modify-macro*のようなものを定義してみても良いかもしれません。

まとめ

どうせコンパイル時に展開すると思うのでVALUES Forms as Placesを処理できても良いと思うのですが、何か事情があるのでしょうか。

これらのフォームは、ループ内で頻出しそうなので極力無駄のない定義になる必要があるような気はしますが、展開時に無駄は省けそうですし、さて真相やいかに……。


HTML generated by 3bmd in LispWorks 7.0.0

Flavorsのメソッドコンビネーションを眺めたり再現してみよう: その二

Posted 2018-12-11 14:47:02 GMT

Lisp メソッドコンビネーション Advent Calendar 2018 11日目 》

前回に引き続き、メソッドコンビネーション元祖のFlavorsに標準装備されていたメソッドコンビネーションを眺めたり再現してみたりしようと思います。

前回いきなりcaseメソッドコンビネーションの再現に取り組んでみましたが、改めて、どんなメソッドコンビネーションがあるのか一覧にしてみたくなりました(ネタ切れだから)

調べてみたのは、後期Flavors〜New Flavorsあたりのメソッドコンビネーションです。

前知識

Flavorsのメソッドコンビネーションの標準構成は、Common Lisp版から:aroundを外したようなものです。
:aroundの部分は、defwrapperというメソッドを囲むマクロを定義する感じになります。

また、キーワードシンボルになっていますが、Lisp Machine Lispでは、userパッケージのシンボルの略記になります。
つまり、(eq :progn 'progn)です。
共用シンボルはuserパッケージに置かないで、keywordパッケージに纏めよう、と整理したのがCommon Lispです。

さて、では眺めてみましょう。

:daemon

Common Lispのstandardメソッドコンビネーションから:aroundを外したようなもの。
これを:darmonと呼びます。

:before / :after

Common Lispと同じです、:daemonメソッドコンビネーションの部品を単体で呼び出しているとも考えられます。

:and / :or / :progn

Common Lispから:aroundを外したようなもの。

:append / :nconc / :list

Common Lispから:aroundを外したようなもの。

:inverse-list

listが逆順になって返ってきます。
わざわざ用意しているからには使い道があるのでしょう……。

:max / :min / :sum

Common Lispと同じですが、:sumは、Common Lispでは+になっていますね。

:case

前回紹介した不思議メソッドコンビネーションです。

:daemon-with-and

:andの前後を:before:afterで囲んだものです。
Common Lispではand:daemonは付きません。

:daemon-with-or

:orの前後を:before:afterで囲んだものです。

:daemon-with-override / :override

:overrideで定義したメソッドが:daemonメソッドコンビネーションの前に配置されていて、それがorで結合しています。
つまり、:overridenilを返した場合だけ、:daemonに進むという変ったものです。

:two-pass

プライマリ全部起動の後に:after全部を起動するものらしいです。
make-instanceで良く使うらしいですが、インスタンス初期値の設定とかでしょうか。

:pass-on

メソッドの返り値を次のメソッドに次々と渡していくメソッドコンビネーションのようです。
先日フィルターを作ろうとしていましたが、pass-onならできそうです。 しかし、返り値は何故か多値で返す様子。。
(a b c)という引数を持つメソッドを連鎖させるには、(values a b c)で値を渡さないといけないようですが、何故なのか。

ネタ切れなので、次回からCommon Lispで、Flavorsのメソッドコンビネーションを、`Symbolics Open Genera上のFlavorsの挙動を確認しつつ再現していきます。


HTML generated by 3bmd in LispWorks 7.0.0

実践SETF定義:define-setf-expanderで型破りなsetf構文を作ろう

Posted 2018-12-10 17:46:17 GMT

Lisp SETF Advent Calendar 2018 11日目 》

setf関数、defsetfと紹介してきましたが、今回は一番汎用的なdefine-setf-expanderの紹介です。
実際、defsetfや、define-modify-macroも、define-setf-expanderの定義に展開している処理系も多いです。

define-setf-expanderを簡単に説明すると、

(setf (x y z) a b c)

のようなフォームのxyzabcという部品を好きなように配置することが可能です。

好きなように、といっても一応xyzは変数としての振舞い、abcは値としての振舞いをする必要はありますので、作法に則る必要はあります。

CPLの代入構文のallを作ってみよう

左辺値について調べている際に、CPLが左辺値、右辺値を整理したとWikipediaに書いてあったので、ちょっと眺めてみましたが、Fortran、Algolに比較すれば、左辺値が拡張された感じはします。

左辺値にリスト表現や配列をとって、変数を複数同時に代入できたり、リストを分解して変数に代入できたり、1963年の言語にしては先進的ですが、拡張のバリエーションとして、

all a, b, c := 0

という書法がありました。

上記の場合は、変数全部に0が代入されるわけですが、左辺と右辺で微妙に対称性がなく、イレギュラーなsetf定義の例に良さそうなので、ちょっと考えてみましょう。
まず、構文の見た目ですが、こんな感じになるかなと思います。

(setf (all a b c) 0)

(list a b c)(0 0 0)

構文全体としては直感的なのですが、allというゲッターを考えるに、これは単独では存在できそうにないですが、どうなんでしょう。
それはさておきこんな感じに書いてみました。

(define-setf-expander all (&rest places &environment env)
  (loop :with store := (gensym "all-")
        :for p :in places
        :for (d v sv setter getter) := (multiple-value-list (get-setf-expansion p env))
        :append d :into ds
        :append v :into vs
        :append sv :into svs
        :collect setter :into setters
        :collect getter :into getters
        :finally (return 
                  (values ds
                          vs
                          `(,store)
                          `(let (,@(mapcar (lambda (v)
                                             `(,v ,store))
                                           svs))
                             (values ,@setters))
                          `(values ,@getters)))))

下記のフォームをマクロ展開してみると、

(let ((x 0)
      (y 0)
      (z 0))
  (incf (all x y z) 100)
  (list x y z))(100 100 100)

こんな感じになります。

(let ((x 0) (y 0) (z 0))
  (let* ()
    (let* ()
      (let ((#:|all-128690| (+ (values x y z) 100)))
        (let ((#:|Store-Var-128691| #:|all-128690|)
              (#:|Store-Var-128692| #:|all-128690|)
              (#:|Store-Var-128693| #:|all-128690|))
          (values (setq x #:|Store-Var-128691|)
                  (setq y #:|Store-Var-128692|)
                  (setq z #:|Store-Var-128693|))))))
  (list x y z))

大体の場所でallは機能が成立するようです。

(let ((x (list 0 1 2))
      (y 0)
      (z 1))
  (setf (all (values (car x) (cadr x)) y z) 
        42)
  (list x y z))((42 42 2) 42 42) 

(let ((v (make-sequence 'vector 7))) (setf (all (elt v 1) (elt v 3) (elt v 5)) '- (all (elt v 0) (elt v 2) (elt v 4) (elt v 6)) '+) v) → #(+ - + - + - +)

しかし、pushpopの挙動は良く分かりません。
push/popした後のリストがallによって同値になるので、これであってるような間違っているような。

(let ((x (list 1 2 3))
      (y (list 0 0 0)))
  (push 'a (all x y))
  (list x y))((a 1 2 3) (a 1 2 3)) 

(let ((x (list 1 2 3)) (y (list 0 0 0))) (pop (all x (cdr y))) (list x y))((2 3) (0 2 3))

まとめ

変った代入構文をみつけたら、また定義に挑戦してみようと思います。


HTML generated by 3bmd in LispWorks 7.0.0

Flavorsのメソッドコンビネーションを再現してみよう: case篇

Posted 2018-12-09 20:59:45 GMT

Lisp メソッドコンビネーション Advent Calendar 2018 10日目 》

組み込みのメソッドコンビネーションの紹介も尽きてきたので、メソッドコンビネーション元祖のFlavorsに標準装備されていたメソッドコンビネーションの再現でもしてみましょう。

Flavorsのcaseメソッドコンビネーション(の真似のつもり)

Flavorsのcaseメソッドコンビネーションと題しつつオリジナルの挙動が確認できていないのですが、どうもFlavorsとNew Flavorsで挙動が違うようです。

再現したつもりなのは、New Flavorsの方ですが、もしかすると、第二修飾子がメソッドの引数に現れた場合のディスパッチにcaseを使うのかもしれません。

作成してみたものは、メソッドの引数を安直にcaseに展開するもので、第二修飾子がcaseのマッチ対象になります。

(define-method-combination case ()
  ((case-clauses (case . *)))
  (:arguments a)
  (loop :for c :in case-clauses
        :if (equal '(case otherwise) (method-qualifiers c))
        :collect c :into otherwise
        :else 
        :collect `(,(cadr (method-qualifiers c)) (call-method ,c nil)) :into clauses
        :finally 
        (return 
         `(case ,a
            ,@clauses
            ,@(and otherwise 
                   `((otherwise
                      ,(reduce (lambda (m ms)
                                 `(call-method ,m ,(and ms `((make-method ,ms)))))
                               otherwise
                               :initial-value nil
                               :from-end T))))))))

(defgeneric fib (n)
  (:method-combination case))

(defmethod fib case 0 (n) n)

(defmethod fib case 1 (n) n)

(defmethod fib case otherwise ((n number)) (+ (fib (1- n)) (fib (- n 2))))

(defmethod fib case otherwise ((n integer)) (call-next-method))

(fib 20) → 6765

展開を確認してみるとこんな感じです

(mc-expand #'fib 
           'case
           nil
           20)(case a
  (1 (call-method #<standard-method fib (case 1) (t) 41E00640EB> nil))
  (0 (call-method #<standard-method fib (case 0) (t) 41E00641C3> nil))
  (otherwise
   (call-method
    #<standard-method fib (case otherwise) (integer) 41E0061D63>
    ((make-method
      (call-method
       #<standard-method fib (case otherwise) (number) 41E0059333>
       nil)))))) 

eql特定子をメソッドコンビネーションで実装してしまっている感がありますが、汎用化を推し進めると、filtered-functionみたいなことになるのかなと思います。

今後、Flavors、New Flavorsのcaseメソッドコンビネーションの挙動が確認できたら、オリジナルに忠実なものも作成してみようと思います。


HTML generated by 3bmd in LispWorks 7.0.0

define-method-combinationの短形式は使えるか

Posted 2018-12-09 10:54:06 GMT

Lisp メソッドコンビネーション Advent Calendar 2018 9日目 》

前回メソッドコンビネーションのlist:aroundと組み合わせることで結構汎用的に使えることが分かってしまいました。
+minmaxappendnconcのメソッドコンビネーションを眺めると、どれも&restな引数の関数なので、listメソッドコンビネーションでできた結果を:aroundapplyすれば良いだけです。

define-method-combinationの短形式は指定したシンボルのフォームで囲むメソッドコンビネーションを定義しますが、関数以外のオペレーターも指定することが可能です。

define-method-combinationの短形式の存在意義を確認するため、clパッケージ内のフォームを物色してみました。

define-method-combinationの短形式で定義できそうなものを探す

ということで、引数が&rest&bodyかで始まるフォームをclパッケージから探してみましたが、下記のようになりました。

(ql:quickload :trivial-arguments)

(loop :for s :being :the :external-symbols :of :cl :when (and (fboundp s) (ignore-errors (typep (trivial-arguments:arglist s) '(cons (member &rest &body) *)))) :collect s)

(rotatef shiftf cond ignore-errors append logeqv tagbody and locally logand lcm psetf trace vector in-package with-standard-io-syntax gcd psetq setq logior nconc or list progn make-broadcast-stream values untrace loop make-concatenated-stream declaim logxor + *)

なるほど、碌な応用が考えつかないものばかりです。
標準で提供されているメソッドコンビネーションは、このリストの中でも割合に有用なものが選ばれていたことも判ります。

とりあえず、幾つか定義を試してみましょう

ignore-errorsメソッドコンビネーション

define-method-combinationの短形式で順次実行のフォームを定義した場合、基本的にprognと同じようになります。

(define-method-combination ignore-errors)

(defgeneric foo (x y) (:method-combination ignore-errors))

(defmethod foo ignore-errors (x y) (+ x y))

(defmethod foo ignore-errors ((x list) (y list)) (append x y))

(foo 1 2) → 3

(foo '(0 1 2) '(a b c)) → nil #<conditions:arithmetic-type-error 40201FF25B>

こんな感じに展開されるのですが、エラーが発生してもとにかく続行したいという時には使えるかもしれません。

ignore-errorsメソッドコンビネーションは、prognメソッドコンビネーションを`ignore-errorsで囲めば定義せずに済んでしまいそうです。
andorメソッドコンビネーションと:aroundignore-errors適用も良さそうです。

一応の展開形確認

(mc-expand #'foo 
           'ignore-errors
           nil
           '(0 1 2)
           '(a b c))

(ignore-errors (call-method #<standard-method foo (ignore-errors) (list list) 402014EFD3> nil) (call-method #<standard-method foo (ignore-errors) (t t) 40201FE2C3> nil))

valuesメソッドコンビネーション

listメソッドコンビネーションに:aroundvalues-listすれば良いのですが、より直截的でしょうか。

(define-method-combination values)

(defgeneric foo (x y) (:method-combination values))

(defmethod foo values (x y) (list x y))

(defgeneric bar (x) (:method-combination values))

(defmethod bar values ((x number)) (list x 'number))

(defmethod bar values ((x rational)) (list x 'rational))

(defmethod bar values ((x float)) (list x 'float))

(defmethod bar values ((x integer)) (list x 'integer))

(bar 1.0)(1.0 float) (1.0 number)

まあ、頭を捻れば応用例も見出せるかもしれません。

loopメソッドコンビネーション

loopで囲んでみましたが、メソッドコンビネーションスコープとループのスコープが微妙に噛み合いません。
長形式で工夫して定義するかMOPでメソッドコンビネーションの枠組み自体を書き換えるかしないと、あまり有用な定義はできなさそうです。

噛み合わない点ですが、loopblockスコープが指定できない、等々です。
これもprognメソッドコンビネーションで、条件が成立するまでループさせるような:aroundを書いた方が有用かもしれません。

(define-method-combination loop)

(defgeneric baz (x) (:method-combination loop))

(defmethod baz loop (x) (print x) (throw 'baz x))

(defmethod baz loop ((x number)) (print (list (incf x) 'number)))

(defmethod baz loop ((x rational)) (print (list (incf x) 'rational)))

(defmethod baz loop ((x integer)) (print (list (incf x) 'integer)))

(defmethod baz :around (x) (catch 'baz (call-next-method)))

(baz 8)(9 integer)(9 rational)(9 number) ▻ 8 → 8

tagbodyメソッドコンビネーション

折角なので常軌を逸していそうなtagbodyについて考えてみましたが、blockや、tagbodyのタグのスコープはダイナミックエクステントなので、普通に書いたらメソッドを跨ぐことができません。
ということで、定義がtagbodyblockの中に収まっている必要があります。

(define-method-combination tagbody)

(defgeneric fact (n) (:method-combination tagbody))

(block nil (let ((n 20) (ans 1)) (tagbody (defmethod fact tagbody ((n (eql 0))) (go X)) (defmethod fact tagbody ((n integer)) (setq ans (* n ans)) (go L)) L (decf n) (fact n) X (return ans)))) → 121645100408832000

まとめ

define-method-combinationの短形式を使って有用なものを定義できることは、ほぼなさそうです。
関数系であれば、listメソッドコンビネーション & :aroundの組み合わせで、順次実行フォーム系であれば、progn & :aroundの組み合わせを、非常に簡潔に書ける、ということがメリットですが、長形式だけ用意しておけば良かったのではないかナー、という結論です。


HTML generated by 3bmd in LispWorks 7.0.0

setf系アイデアの最終形態: letf

Posted 2018-12-08 19:29:08 GMT

Lisp SETF Advent Calendar 2018 9日目 》

今回は、また毛色を変えて、setf系の面白構文を紹介したいと思います。

letf

letfは、Lisp Machine Lispに存在したletsetfが合体したような構文です。
束縛系構文なので、代入系のsetfとはちょっと違うのですが、こんな感じで使えます。

(defstruct s x y z)

(let ((s (make-s :x 0 :y 1 :z 2))) (letf (((s-x s) 42)) (print s) (list s (s-x s) (s-y s) (s-z s)))) ▻ #S(s :x 42 :y 1 :z 2)(#S(s :x 0 :y 1 :z 2) 42 1 2)

letの変数部分が、setfでいう一般化変数になっていて、上記の場合は、letfのボディの中では構造体sxスロットは42になっていますが、スコープを抜ければ復帰します。

Lisp Machine Lispでは、ロカティブという参照の仕組みがありますが、ロカティブをすげかえる仕組みがマシンのメモリ参照のレベルで実装されていて、この機能によってCommon Lispでいうスペシャル変数のような感じで色々なものの参照を扱えるのですが、letfはこれを活用しています。

Common LispでもLispWorksのような一部の処理系には、letfがユーティリティとして含まれていますが、代入して元に戻すフォームをunwind-protectで囲んだ実装です。

Emacs Lispのcl.elにも何故かletfがありますが、こちらも代入して復帰となっています。
※註: Common Lispにはletfはありません。

なお、代入して元に戻す方式と、スペシャル変数のようなバインディング方式の違いですが、マルチプロセス時等に挙動が変ってきます。

また、letfで扱えるのは、ロカティブのみです。
setfでは読み出しと書き込みで整合性がなかったり非対称なものを定義できますが、こういうものはletfでは扱えません。

参考


HTML generated by 3bmd in LispWorks 7.0.0

定番メソッドコンビネーション紹介: list

Posted 2018-12-08 08:45:11 GMT

Lisp メソッドコンビネーション Advent Calendar 2018 8日目 》

今回はCommon Lispの組み込みメソッドコンビネーションのlistを紹介していきます。

listメソッドコンビネーション

listは、メソッドの集合をlistで囲んだように実行するもので、全体の結果がリストとして返ってきます。

(defgeneric foo (x)
  (:method-combination list))

(defmethod foo list ((x number)) (cons x 'number))

(defmethod foo list ((x rational)) (cons x 'rational))

(defmethod foo list ((x float)) (cons x 'float))

(defmethod foo list ((x integer)) (cons x 'integer))

(foo 1)((1 integer) (1 rational) (1 number))

こんなのどこで使うんだと思ってしまいますが、あれこれ考える中で、返り値がリストということは、:aroundで色々変形してやれば、結構汎用的な使い方もできることに気付きました。

多値で返す

(defmethod foo :around (x)
  (values-list (call-next-method)))

(foo 1)(1 integer) (1 rational) (1 number)

任意の集約

(defmethod foo :around (x)
  (reduce (lambda (x ans)
            `(,(+ (car x) (car ans))
              ,(cons (cadr x) (cadr ans))))
          (call-next-method)
          :initial-value '(0 nil)
          :from-end T))

(foo 1)(3 (integer rational number))

標準の組み込みメソッドコンビネーションで結果を集約する関数系のものに、+minmaxappendnconcがありますが、これ等は、list+:aroundで集約で実現できてしまいそうです。

自ら今後紹介するメソッドコンビネーションのネタを潰してしまった感もありますが、次回も、標準組み込みのメソッドコンビネーションを紹介したりしようかなと思います。


HTML generated by 3bmd in LispWorks 7.0.0

実践SETF定義: defsetf長形式篇

Posted 2018-12-07 18:38:26 GMT

Lisp SETF Advent Calendar 2018 8日目 》

実践編今回は、defsetfの長形式篇です。

前回、defsetfの短形式を紹介しましたが、短形式では、引数の順番等の条件がうまく嵌ればとても簡潔に書けます。
しかし、そうはいかない場合もあるのと、仮引数で&optional&key等々と指定したい場合にdefsetfの長形式を使うことになります。

短形式と同じ例ですが、こんなゲッターとセッターの対があった場合、

(defun kar (list)
  (car list))

(defun set-kar (list val) (rplaca list val))

(let ((u (list 0 1 2))) (set-kar u 42) (list u (kar u)))((42 1 2) 42)

(defsetf kar (list) (val)
  `(set-kar ,list ,val))

のように、普通のマクロ定義のような感じで書けます。

(let ((u (list 0 1 2)))
  (setf (kar u) 42)
  (list u (kar u)))((42 1 2) 42) 

引数宣言の順番も、setfが使われる場合の引数の順番と同じなので迷うこともないでしょう。

短形式では、非常に簡潔に書けました、

(defsetf kar set-kar)

しかし、長形式で明示的に書かれていた方が判り易いですね。

defsetfで定義できるものは基本的にセッターとゲッターが存在し、それが対称に動作しているものになります。

defsetfで扱えないようなイレギュラーなものは、最も汎用的なsetf定義オペレーターである、define-setf-expanderget-setf-expansionを駆使することになります。

アドベントカレンダーの後半ではイレギュラーなsetf定義でも紹介していこうかなと思います。


HTML generated by 3bmd in LispWorks 7.0.0

定番メソッドコンビネーション紹介: progn

Posted 2018-12-06 17:46:30 GMT

Lisp メソッドコンビネーション Advent Calendar 2018 7日目 》

今回はCommon Lispの組み込みメソッドコンビネーションのprognを紹介していきます。

prognメソッドコンビネーション

prognは、メソッドの集合をprognで囲んだように実行するものです。

メソッドコンビネーションアドベントカレンダー 5日目で、フィルターのパターンをメソッドコンビネーションですっきり書けないか考えていましたが、アイテムの一時置き場を用意して更新していくことが可能であれば、prognで書けそうだと思ったので試してみました。

メニューにはアイテム置き場とフィルター適用済アイテムを持たせて、初期化とソートを:aroundで、他各種フィルターの適用は、prognメソッドコンビネーションで実行してみています。

(defclass menu ()
  ((items :initform '() :initarg :items :accessor items)
   (filtered-items :initform '() :initarg :items :accessor filtered-items)))

(defclass fizz-filtered-menu (menu) ())

(defclass buzz-filtered-menu (menu) ())

(defclass *-filtered-menu (fizz-filtered-menu buzz-filtered-menu) ())

(defgeneric filter (menu) (:method-combination progn))

(defmethod filter :around ((menu *-filtered-menu)) (setf (filtered-items menu) ;filtered-items初期化 (items menu)) (call-next-method) ;フィルタリング (setf (filtered-items menu) ;昇順にソート (sort (filtered-items menu) #'<)) menu)

(defmethod filter progn ((menu fizz-filtered-menu)) (setf (filtered-items menu) (remove-if-not (lambda (x) (zerop (mod x 3))) (filtered-items menu))) menu)

(defmethod filter progn ((menu buzz-filtered-menu)) (setf (filtered-items menu) (remove-if-not (lambda (x) (zerop (mod x 5))) (filtered-items menu))) menu)

フィルターを掛けるためだけに、mixin的なクラスを定義するのが、微妙な気がしますが、まあ良しとしましょう。
実行してみましょう。

(let ((menu (make-instance '*-filtered-menu 
                           :items (loop :repeat 100 :collect (random 100)))))
  (filtered-items (filter menu)))(0 15 30 45 45 60 60 60 75) 

先日定義してみたmc-expandで展開形を確認してみると、:aroundで囲まれたprognということが判ります。

(mc-expand #'filter 'progn nil (make-instance '*-filtered-menu))(call-method 
 #<standard-method filter (:around) (*-filtered-menu) 41C008E0CB>
 ((make-method
   (progn
     (call-method #<standard-method filter (progn) (fizz-filtered-menu) 402003E7FB> nil)
     (call-method #<standard-method filter (progn) (buzz-filtered-menu) 4020052C8B> nil))))) 

:around修飾子の謎 その二

:aroundは、メソッド単体を囲んで実行するというよりは、コンビネーション全体を取り囲うものなのでは? Flavorsのdefwrapperのような使い方をするのではないか? と昨日書きましたが、改めてSonya Keene氏 のObject-Oriented Programming in Common Lisp(邦訳:Common Lispオブジェクト指向)を確認したところ、「5. Controlling the Generic Dispatch」の章でちゃんとそんな風に説明してました。
前に読んでた筈でしたが、すっかり忘れていたようです……。

Object-Oriented Programming in Common Lisp(邦訳:Common Lispオブジェクト指向)は、Common Lispにオブジェクトシステムを導入するのにあたって貢献した人達(主にSymbolics)が執筆に協力している本なので、割合に説明が細かいです。

また、多くの書籍では、Common Lispと他の言語のオブジェクトシステムとの対比でオブジェクトシステムに変な機能があるとか無いとかで説明されることも多いですが、この本は、Common Lispネイティブな視点で書かれている所もお勧めです(ANSI CL成立前の仕様だったりするのが若干残念)

次回も、標準組み込みのメソッドコンビネーションを紹介したりしようかなと思います。


HTML generated by 3bmd in LispWorks 7.0.0

定番メソッドコンビネーション紹介: and

Posted 2018-12-05 18:10:00 GMT

Lisp メソッドコンビネーション Advent Calendar 2018 6日目 》

今回はCommon Lispの組み込みメソッドコンビネーションのandを紹介していきます。

andメソッドコンビネーション

standardはパターンに分類するなら呼び出しのフックの実現でしたが、andは、メソッドの集合をandで実行するものです。

二次元座標2dと、それを継承した3d、ついでに何故か2dのスーパークラスである1dがある場合を考えてみましょう。

(defclass 1d ()
  ((x :initarg :x :accessor x)))

(defclass 2d (1d) ((y :initarg :y :accessor y)))

(defclass 3d (2d) ((z :initarg :z :accessor z)))

これらの等価を判定する==を考えてみると、andを使えば、

(defgeneric == (x y)
  (:method-combination and))

(defmethod == and ((a 1d) (b 1d)) (= (x a) (x b)))

(defmethod == and ((a 2d) (b 2d)) (= (y a) (y b)))

(defmethod == and ((a 3d) (b 3d)) (= (z a) (z b)))

こんな風にすっきり書けます。

(== (make-instance '1d :x 10)
    (make-instance '1d :x 10))
→ t 

(== (make-instance '2d :x 10 :y 20) (make-instance '2d :x 10 :y 25)) → nil

(== (make-instance '3d :x 100 :y 50 :z 80) (make-instance '3d :x 100 :y 50 :z 80)) → t

(== (make-instance '3d :x 100 :y 50 :z 80) (make-instance '3d :x 100 :y 50 :z 8)) → nil

前回定義してみたmc-expandで展開形を確認してみると、こんな感じに、実際にandでメソッドが囲まれています。
※なお展開形は実装依存です。LispWorksでは実際は、sys::blocked-andが使われています。

(mc-expand #'== 
           'and
           nil 
           (make-instance '3d :x 100 :y 50 :z 80)
           (make-instance '3d :x 100 :y 50 :z 8))(and (call-method #<standard-method == (and) (3d 3d) 41D052D4A3>
                  nil)
     (call-method #<standard-method == (and) (2d 2d) 41D052D48B>
                  nil)
     (call-method #<standard-method == (and) (1d 1d) 41D04E5EE3>
                  nil))

さらに、andは、:around修飾子も持っているので、実行の全体を何かの処理で包みたい場合に使えます。

(defvar *debug* nil)

(defgeneric == (x y) (:method-combination and))

(defmethod == and ((a 1d) (b 1d)) (when *debug* (print (list (x a) (x b)))) (= (x a) (x b)))

(defmethod == and ((a 2d) (b 2d)) (when *debug* (print (list (y a) (y b)))) (= (y a) (y b)))

(defmethod == and ((a 3d) (b 3d)) (when *debug* (print (list (z a) (z b)))) (= (z a) (z b)))

(defmethod == :around (x y) (let ((*debug* T)) (call-next-method)))

(== (make-instance '3d :x 100 :y 50 :z 80) (make-instance '3d :x 1 :y 50 :z 80))(80 80)(50 50)(100 1) → nil

:around修飾子の謎

メソッドコンビネーションアドベントカレンダーを書き始める前は、and等でも:aroundが使えるのを知りませんでした。
使えることを知ってからも一体何に使うのか謎でしたが、Flavorsにあった、defwrapperのようなパターンを記述するためなのかもしれません。
そう考えると:aroundが最外周に配置されることもなんとなく分かる気がしますが、 上記の例は、ちょっとわざとらしい例ながらもdefwrapperの使い方に近いです。

次回も、標準組み込みのメソッドコンビネーションを紹介したりしようかなと思います。


HTML generated by 3bmd in LispWorks 7.0.0

実践SETF定義: defsetf短形式篇

Posted 2018-12-05 16:45:28 GMT

Lisp SETF Advent Calendar 2018 6日目 》

実践編今回は、defsetfの短形式篇です。ネタは刻んでいきます。

defsetfや、define-method-combinationでは、良く使いそうなものを短く書ける短形式と、詳細にパラメータを記述できる長形式がありますが、そんなに頻繁に使うわけでもないので、短形式があることで逆に混乱を招いている気がします……。

そんな短形式ですが、既存のゲッターとセッターが違う名前で存在する場合に、ゲッターの名前に統一するような場合に使えます。

具体例としては、こんな対があった場合、

(defun kar (list)
  (car list))

(defun set-kar (list val) (rplaca list val))

(let ((u (list 0 1 2))) (set-kar u 42) (list u (kar u)))((42 1 2) 42)

defsetf一発で(setf kar)に纏められます。

(defsetf kar set-kar)

しかし、セッター側の引数の順番に注意。
うまくはまるには、変数→値の順である必要があります。

(let ((u (list 0 1 2)))
  (setf (kar u) 42)
  (list u (kar u)))((42 1 2) 42) 

処理系の基本関数などの定義では、結構使われているようですが、ユーザーが書くプログラムで、バラバラの名前のゲッター、セッターをsetfのもとに統一する、ということはあまりなさそうです。
長形式だけを用意しておいて、パターンが頻出する場合には、ユーザにマクロを書かせる、という方針でも良かったのでは……。

次回は、defsetfの長形式を解説してみます。


HTML generated by 3bmd in LispWorks 7.0.0

カジュアルにメソッドコンビネーション定義してみよう

Posted 2018-12-05 07:11:18 GMT

Lisp メソッドコンビネーション Advent Calendar 2018 5日目 》

組み込みのメソッドコンビネーションのパターンを淡々と紹介していこうかなと思いましたが、eshamsterさんの記事が面白かったので、自分も簡単なものを考えて定義してみることにしました。

フィルターでメソッドコンビネーションが使えないか

たまたま今日GUIの一覧メニューのフィルターを作成していて、フィルターってメソッドコンビネーションで上手く書けるのでは?と思ったので試してみます。
とりあえず、リスト対して絞り込みの関数を順にandな感じで適用していく、という感じです。
通常なら、下記のようにでも書くのかなという所です。

(flet ((filter-integerp (list)
         (remove-if-not #'integerp list))
       (filter-evenp (list)
         (remove-if-not #'evenp list))
       (filter-flatten (list)
         (flatten list)))
  (defparameter *fns* (list #'filter-flatten
                            #'filter-integerp
                            #'filter-evenp)))

(reduce (lambda (res f) (funcall f res)) *fns* :initial-value '((1 2 3 4 (1 2 3 4 nil t 8) t 8) (1 2 3 4 (1 2 3 4 nil t 8) t 8)))(2 4 2 4 8 8 2 4 2 4 8 8)

フィルターをメソッドコンビネーションで書いてみよう

下準備

メソッドコンビネーションを定義するにあたって、コンビネーションがどう展開されるかを確認しつつ書きたいので展開ユーティリティを定義すると良いかなと思います。

(ql:quickload :closer-mop)

(defun mc-expand (gf mc mcopts &rest args) (c2mop:compute-effective-method gf (c2mop:find-method-combination gf mc mcopts) (compute-applicable-methods gf args)))

これでこんな感じに展開できます。

(mc-expand #'foo 
           'chain
           '()
           '(1 2 3 4 nil t 8))(progn
  (call-method
   #<standard-method foo (chain evenp-filter) (list) 4020328873>
   ((make-method
     (call-method
      #<standard-method foo (chain integerp-filter) (list) 4020231D53>
      nil)))))  

展開を確認しながら書けるのであれば、謎のdefine-method-combinationも使いこなせる気がしてきます。

書いてみる

さて、とりあえずは、フィルター適用の順序不定で良しとして、こんな感じに書いてみました。
標準のメソッドコンビネーションでは、:before:after等、修飾子の指定は一つですが、実際はこの部分はリストになっていて、equalで等価判定されます。
定義されるメソッドが違う名前であれば、クラス関係に関係なく増やすことが可能です。

(define-method-combination chain ()
  ((methods (chain . *)))
  `(progn 
     ,(reduce (lambda (m ms)
                `(call-method ,m ,(and ms `((make-method ,ms)))))
              methods
              :initial-value nil
              :from-end T)))

(defgeneric foo (x) (:method-combination chain))

(defmethod foo chain integerp-filter ((u list)) (remove-if-not #'integerp (call-next-method)))

(defmethod foo chain evenp-filter ((u list)) (remove-if-not #'evenp (call-next-method)))

(defmethod no-next-method ((gf (eql #'foo)) method &rest args) (apply #'identity args))

(foo '(1 2 3 4 nil t 8))
→ (2 4 8) 

call-next-methodで次を呼ばないといけないのがめんどうな気がしますが、処理結果を次に渡す安直で良い方法が分かりません(メソッドを分解して関数を取り出しても良いのですが……)
順不同で登録/呼びだしをするのでno-next-methodでデフォルト値を返すようにしています。

やっぱり順番を指定したい

やっぱり適用順によってはエラーになるようなフィルター構成もあるので、順番は指定しても良いかなと思ったので、優先順位を指定してみることにしました。

(defun ordered-chain-qualifier-p (method-qualifiers)
  (typep method-qualifiers
         '(cons (eql ordered-chain) (cons integer (cons T null)))))

(define-method-combination ordered-chain () ((methods ordered-chain-qualifier-p)) (let ((methods (remove-duplicates methods :key (lambda (method) (third (method-qualifiers method))) :from-end T))) `(progn ,(reduce (lambda (m ms) `(call-method ,m ,(and ms `((make-method ,ms))))) (stable-sort methods #'> :key (lambda (method) (second (method-qualifiers method)))) :initial-value nil :from-end T))))

(defgeneric bar (x) (:method-combination ordered-chain)))

(defmethod bar ordered-chain 0 identity ((u list)) u)

(defmethod bar ordered-chain 100 integerp-filter ((u list)) (remove-if-not #'integerp (call-next-method)))

(defmethod bar ordered-chain 200 evenp-filter ((u list)) (remove-if-not #'evenp (call-next-method)))

(defmethod bar ordered-chain 1 flatten-filter ((u list)) (flatten (call-next-method)))

展開はこんな感じです。

(mc-expand #'bar
           'ordered-chain
           '()
           '(1 2 3 4 nil t 8))(progn
  (call-method
   #<standard-method bar (ordered-chain 200 evenp-filter) (list) 4020447F83>
   ((make-method
     (call-method
      #<standard-method bar (ordered-chain
                             100
                             integerp-filter) (list) 402031D65B>
      ((make-method
        (call-method
         #<standard-method bar (ordered-chain
                                1
                                flatten-filter) (list) 4020458C5B>
         ((make-method
           (call-method
            #<standard-method bar (ordered-chain 0 identity) (list) 4020467533>
            nil)))))))))))

メソッドコンビネーション修飾子の二番目に整数で優先度を付けてみました。
パターンマッチでも行けると思いますが、ordered-chain-qualifier-pを定義して選別しています。

メソッドコンビネーション修飾子はequalで判定されると上述しましたが、優先度を変更して再定義するとメソッドがどんどん増えてしまうので、重複も削除しています。
また、デフォルトメソッドを最優先として定義すればno-next-methodの定義も不要になります。

さて実行してみると、

(bar '((1 2 3 4 (1 2 3 4 nil t 8) t 8) (1 2 3 4 (1 2 3 4 nil t 8) t 8)))(2 4 2 4 8 8 2 4 2 4 8 8)  

とりあえずは、3つのフィルターが順に適用されているようです。

問題点

しかし、優先度を付けてみると、今度は、call-next-methodと優先番号の組み合わせが直感的でない気がします。

具体的には、優先番号が低い順から呼ばれるのに、引数の適用は優先度の高い方からされる、ということで、call-next-methodで呼ばない場合、連鎖は優先度が低い方から途切れてしまいます。

call-methodの入れ子にしないで、平坦に優先順に並べ、適用結果を次々に受け渡すことができれば、整合性は取れる筈ですが……。

メソッドコンビネーションアドベントカレンダー終了まで良い解決策が思い付けば記事のネタにしたいと思います。


HTML generated by 3bmd in LispWorks 7.0.0

TAOの!

Posted 2018-12-04 17:31:40 GMT

Lisp SETF Advent Calendar 2018 5日目 》

今回は、TAO/ELISの!!!についてです。

実の所、TAO/ELISの!!!は、setfとは挙動は似ているものの実現方法が異なったもので、Common Lispのようにマクロに展開されるものではなく、(!という特別な括弧が作る代入フォームです。

詳しくは、1986年のbit誌のマルチパラダイム言語 TAO 連載で解説されていますので興味のある方は参照してみると良いかと思います。

この!記法は、setf!になった感じですが、上述のように特殊な括弧です。

(!(car x) 42)(setf (car x) 42) 

このフォームは読み書きの場所を返すようになっている(マシンの支援あり)ので、

(!(if pred (car x) x) 42)

のようなものもsetfのような事前のマクロ定義なしに書くことが可能です。

!!の方は自己代入式といって、これも面白くて便利なのですが、フォームの結果を代入する場所を!で指定して書き込むというものです。

これを使うと(setq x (+ x 42))のような良くある代入が、

(!!+ !x 42)

のように書けます。

(!!cdr !x)(pop x)

(!!nconc !(car x) y)(setf (car x) (nconc (car x) y))

(!!delete 'a !x)(setq x (delete 'a x))

等々入り組んだ表現になる程シンプルに書けたりします。

ちなみに、このアイデアは、1979のTAO/60にはあったようで、PDP-11上に実装された初期のTAO/60について書かれた、TAO LISP についての5. 複値形式 (Double valued form)の段落で説明があります。

これによると、

複値形式の概念に思い至ったのは, ALGOL68 や Pascal のプログラムを Lisp に移し替えようとしたとき, どうしてもスムーズにいかなかった悔しい経験があったからである.

とのことなので、やはりAlgol系言語の左辺値の書法が念頭にあったようです。

記法は、TAO/ELISの!ではなく、:を使っています。
TAO/ELISでは、Common Lispに接近したため、キーワードシンボルと記法がぶつかってしまうために!になったようです。 (TAO/ELIS復活祭(2010)で竹内先生が軽く説明していました)

インタプリタの軽快な動作に力を入れていたTAO/ELISでしたが、setfのようにコンパイル前提のマクロでの実現ではない方向で工夫を凝らした代入機構の紹介でした。

ちなみにCommon Lispでも、リーダーマクロで(を再定義すれば、そこそこ似た感じに記述することは可能です。
興味のある方は挑戦されてみてはいかがでしょうか。


HTML generated by 3bmd in LispWorks 7.0.0

実践SETF定義: #'(setf foo)篇

Posted 2018-12-03 16:50:53 GMT

Lisp SETF Advent Calendar 2018 4日目 》

どうもsetf機構についてどんどんマイナーな方向に進んでしまいそうなので、実践的なものも挟んでいきたいと思います。

Common Lispでのsetfの定義方法と定義構文ですが、やたらと数が多いです。
しかも、省略形式と、詳細形式に分かれていたりして、一体何を使ったら良いのかと思うこともままあります。

ざっと一覧にすると、

  • 自動アクセサ生成系(defclassdefstruct)
  • Macro Forms as Places/Symbol Macros as Places で自動で定義される系
  • (defun/defmethod (setf ...))
  • defsetf
  • define-setf-expander
  • define-modify-macro

加えて、定義時に使う補助関数として、get-setf-expansionがあります。

今回は、上記から、(defun/defmethod (setf ...)) を解説したいと思います。

(defun/defmethod (setf ...))

(defun/defmethod (setf ...)) の形式は、CLtL1には存在しておらず、ANSI CLになってから追加されたものです。

追加の目的ですが、メソッドのアクセサをdefmethodで定義させたいためです。

(defclass foo ()
  (a b c))

(defmethod (setf foo-a) (val (obj foo)) (setf (slot-value obj 'a) val))

こんな感じのことがしたいという訳ですが、ANSI CLでは、この為に関数名を拡張して、(setf foo)というような名前も使えるようにしました。
なお、この形式の関数名はsymbol-functionでは扱えないのでfdefinitionを利用します。(専らfdefinitionを使っておけば良いのですが)

(funcall #'(setf foo-a) 42 (make-instance 'foo))
→ 42

(setf (foo-a (make-instance 'foo)) 42) → 42

(fdefinition '(setf foo-a)) → #<standard-generic-function (setf foo-a) 40E01CBF4C>

単なる関数/メソッドなので、setfとは関係ないこともできてしまいますが、当然ながら、これはスタイル上良くないこと、とされています。

(defun (setf foo) (val var)
  (list var val))

(setf (foo 'var) 'val)(var val)

なお、処理系が標準関数のsetfの実装をマクロにするか関数にするかは任意なので、関数で書けるからといって、

(funcall #'(setf car)...)

のような書き方をしても可搬性は期待できません。
個人的には良くやってしまうミスなのですが、統一して欲しい……。

アドベントカレンダーもまだまだ残りの日数がありますので、今後もsetfの定義のバリエーションについて書いていきます。


HTML generated by 3bmd in LispWorks 7.0.0

史上初のSETFを探す旅

Posted 2018-12-02 18:25:23 GMT

Lisp SETF Advent Calendar 2018 3日目 》

探す旅とか書いてますが、LispでSETFのようなものが初めて導入されたのは、結論からいうとLISP 2(1965)だと思います。
しかし、Lispの歴史の文献については、The Evolution of Lispが決定版で良く知られているものなのですが、LISP 2についての記述は何故か殆どありません。
EoLに拠り所にするならば、1973年のL Peter Deutsch氏のByte Lisp(setfq)が最初となるでしょう。

さて、LISP 2ですが、LISP 2は、後世のLispの試行錯誤を先取りしている所が多々ありますが、それはAlgolとの接近による所が多いです。
その後のLispがAlgol化していったから、とも言えますが、具体的には、

  • Algol構文の採用(Algolとコード共用できるレベルを目指す)
  • 型宣言の導入
  • 静的スコープの採用
  • 拡張された左辺値

あたりがあります。

1975年にSchemeがAlgolの影響でLispに静的スコープを取り入れたのが、Lisp史の一つの大きな転換点とされますが、Schemeに10年先行して、LISPをまんまAlgol化しようとしたLISP 2が完全に忘却されているのは不思議ではあります。

なお、LISP 2の発掘については、Paul McJones先生が熱心に取り組まれているようです。

LISP 2は構想のみで実装はなかった、とされていましたが、先日実装も発見されたようです。後世で発掘されるというのも面白い。

さて、この記事ではsetfがメインテーマなので、上記3項目の中では、拡張された左辺値について詳しくみて行くのですが、setf関連については、 LISP II Internal Language(1965) 3.1 ASSIGNMENT-EXPRESSION, LOCATIVES に纏められています。

これによると、(SET locative expression)という形式になっていて、locativeというのがCommon Lispのsetfでいうplaceという感じになっています。
locativeには、ビットやリスト、配列があり、下記のように書けたようです。

(set (car x) b)

(set (cdr x) b)

(set (bit 2 2 a) (bit 2 2 15))

(block ((a real array) (m integer)) (set (a m) 42))

(set (prop (quote x)) (quote (p 42)))

S式は中間言語なので、表層言語でも書いてみると、

car x ← b;

cdr x ← b;

bit (2, 2, a) ← bit (2, 2, 15);

begin real array a; integer m; a[m] ← 42; end;

prop 'x ← '(p 42);

となります。
これらは、Common Lispで書くと、それぞれ、

(setf (car x) b)

(setf (cdr x) b)

(setf (ldb (byte 2 2) a) (ldb (byte 2 2) 15))

(setf (aref a m) 42)

(setf (symbol-plist 'x) '(p 42))

となります。
LISP 2では型宣言するので、配列のsetでは、(配列名 インデックス)という形式でOKのようです。

読み出しと書き込みの記法の二重性について

左辺値について、1960年代初頭のFORTRAN、Algol、CPLあたりの扱いを眺めてみましたが、左辺に配列の記法が来るものは、添字付き変数という概念のようで、配列のアクセサが介在するものではないようです。
LISP 2の記法でもその辺りを踏襲しているように見えますが、locativeという概念で左辺にアクセサの記法が出てくるのは、当時の他の言語と比べてもそれなりに先進的だったのかもしれません。

参考


HTML generated by 3bmd in LispWorks 7.0.0

定番メソッドコンビネーション紹介: standard

Posted 2018-12-01 20:40:27 GMT

Lisp メソッドコンビネーション Advent Calendar 2018 2日目 》

ほぼ無計画なので、内容が続き物だったりそうではなかったりします。
とりあえず、定番というか、Common Lispに標準で用意されているメソッドコンビネーションの紹介でもしていこうかなと思います。

standardメソッドコンビネーション

Common Lispのdefmethodで定義したものは、標準でこのタイプになります。
主な登場人物は、:before:after:aroundcall-next-methodです。
:before:after:aroundは、指定したコンビネーションでのメソッドの追加で利用し、call-next-methodは、優先順位リストに従って次のメソッドを起動します。
他のOOP言語では、superに相当しますが、Common Lispでは、必ずしも継承順位で上位ものを呼び出すわけではないので、call-nextなのでしょう。

とりあえず、実際に:before:after:aroundcall-next-method全部盛りのメソッドを定義して動作を確認してみましょう。

(progn
  (defclass c1 () ())
  (defclass c2 (c1) ())
  (defclass c3 (c2) ()))

(progn (defmethod foo ((o c1)) (format T "~16T~A~%" '(foo c1))) (defmethod foo ((o c2)) (format T "~16T~A~%" '(foo c2)) (call-next-method)) (defmethod foo ((o c3)) (format T "~16T~A~%" '(foo c3)) (call-next-method)) ;; (defmethod foo :before ((o c1)) (format T "~8T~A~%" '(foo c1 :before))) (defmethod foo :before ((o c2)) (format T "~8T~A~%" '(foo c2 :before))) (defmethod foo :before ((o c3)) (format T "~8T~A~%" '(foo c3 :before))) ;; (defmethod foo :after ((o c1)) (format T "~24T~A~%" '(foo c1 :after))) (defmethod foo :after ((o c2)) (format T "~24T~A~%" '(foo c2 :after))) (defmethod foo :after ((o c3)) (format T "~24T~A~%" '(foo c3 :after))) ;; (defmethod foo :around ((o c1)) (format T "~A~%" '(foo c1 :around)) (call-next-method)) (defmethod foo :around ((o c2)) (format T "~A~%" '(foo c2 :around)) (call-next-method)) (defmethod foo :around ((o c3)) (format T "~A~%" '(foo c3 :around)) (call-next-method)))

とりあえず、実行してみるとこんな感じになります。

(foo (make-instance 'c3))(foo c3 around)(foo c2 around)(foo c1 around)(foo c3 before)(foo c2 before)(foo c1 before)(foo c3)(foo c2)(foo c1)(foo c1 after)(foo c2 after)(foo c3 after)
→ nil

大元のメソッドは、プライマリメソッドと呼びますが、call-next-methodで次のメソッドを呼ぶことが可能です。
上記では、(foo c3)(foo c2)で明示的に呼び出していますが、もちろん無ければ呼ばれません。

:beforeは、プライマリメソッド起動の前に起動されますが、クラス優先順位リストの順に該当するものは全て呼び出されます。
:afterは、:beforeと対称の動作です。 なお、:before:afterの中では、call-next-methodは使用できません。

:aroundが割合に複雑ですが、:aroundがあれば、それが最初に起動されます。
その:aroundの中で、call-next-methodが起動されれば、クラス優先順位リスト順に、次の:aroundを起動、:aroundがなければプライマリメソッドを起動します。
call-next-methodを呼べば、その返り値が利用できるので、このデフォルト値を加工するような使い方が殆どです。

:before:afterではcall-next-methodが使えないので、スロットの値を設定する等、副作用目的での利用となります。 GoFのObserverパターンのようなものは、呼び出しイベント時に起動したいフックのようなものが多いので、:before:afterで賄うことが可能かなと思います。

メソッドの中身を覗いてみる

compute-effective-methodで確認すると、call-methodの連鎖が直接見えて判り易いので確認してみると下記のようになります。

call-methodは第一引数に起動するメソッド、第二引数にそれ以降で起動するメソッドのリストを取りますが、入れ子にしていけば、所謂、継続渡しに似た記述になります。
起動リストが空か、メソッドのボディにcall-next-methodの記述がなければ、以降のメソッドは起動されず、そこで処理はストップします。

(c2mop:compute-effective-method #'foo
                                (c2mop:find-method-combination #'foo 'standard nil)
                                (compute-applicable-methods #'foo (list (make-instance 'c3))))

(call-method #<standard-method foo (:around) (c3) 4190211F13> (#<standard-method foo (:around) (c2) 4190211F2B> #<standard-method foo (:around) (c1) 4190211C93> (make-method (progn (call-method #<standard-method foo (:before) (c3) 419021266B> '()) (call-method #<standard-method foo (:before) (c2) 4190212683> '()) (call-method #<standard-method foo (:before) (c1) 419021258B> '()) (multiple-value-prog1 (call-method #<standard-method foo nil (c3) 4190212753> (#<standard-method foo nil (c2) 419021276B> #<standard-method foo nil (c1) 419021269B>)) (call-method #<standard-method foo (:after) (c1) 4190211F43> '()) (call-method #<standard-method foo (:after) (c2) 4190212573> '()) (call-method #<standard-method foo (:after) (c3) 419021255B> '()))))))

元祖Flavorsのデフォルトコンビネーション

元祖Flavorsでは、当初デフォルトのメソッドコンビネーションとして、:daemonコンビネーションが用意されていましたが、登場したのは大体1981年頃のようです。
これは、:before、プライマリ、:afterの組み合わせで、Common Lispのstandardからcall-next-method:aroundを削ったような挙動ですが、1984年あたりになると、:aroundも追加された様子。
call-next-methodのようなものはなく、#'(:method flavor method-name)のような形式で直接呼び出したり、:around専用の継続メソッドを呼び出す構文を使ったようです。

次回は、andコンビネーションあたりを紹介しようと思います。


HTML generated by 3bmd in LispWorks 7.0.0

メソッドコンビネーションってなに?

Posted 2018-12-01 09:50:35 GMT

Lisp メソッドコンビネーション Advent Calendar 2018 1日目 》

Lisp メソッドコンビネーション Advent Calendar 2018始まりました。
開始初日にして既につらい。
setfネタともう一品と思ってテーマにメソッドコンビネーションを選んでみましたが、setf以上にニッチでした。

メソッドコンビネーションってなに?

さて、メソッドコンビネーションについての説明ですが、読んでそのまま、メソッドのコンビネーションのことです。
どういう面の組み合わせかというと、ざっくりメソッドの起動とその順番についてと考えて良いでしょう。

(defclass k0 () ())
(defclass k1 (k0) ())
(defclass k2 (k1) ())

(defmethod m ((obj k2)) 'k2) (defmethod m ((obj k1)) 'k1) (defmethod m ((obj k0)) 'k0)

こんな感じの定義があったとすると、

Common Lispでは、compute-applicable-methodsで起動するメソッドを確認することが可能です。

(compute-applicable-methods #'m (list (make-instance 'k2)))

(#<standard-method m nil (k2) 40E011D08B> #<standard-method m nil (k1) 40E004A153> #<standard-method m nil (k0) 40E006B64B>)

上記の定義では、k0 < k1 < k2 という継承順のクラスに対し、それぞれmを定義しています。

k2についてmを起動してみると、

(m (make-instance 'k2))
→ k2 

となりますが、Common Lispの標準では、compute-applicable-methodsで求めたリストのメソッドが呼ばれていきます。
この例では他2つのメソッドは、呼ばれませんが、明示的な操作と相対的な名前で呼ぶことも可能です。

3つのメソッドのコンビネーションについて説明しましたが、Common Lispでは上記はstandardという名前が付いています。

なんとなく想像が付くかと思いますが、Common Lispでは任意のメソッドの束を任意の構成で呼んだり呼ばなかったりが可能です。

Advice機構とメソッドコンビネーション

メソッドコンビネーションというとbeforeafter等の修飾子を付けてフックを掛ける使い方の印象が強いと思います。

具体的な例でいうと、上記のコードにbeforeafterを追加定義してmを起動すると、

(defmethod m :before ((obj k2)) (print "before k2"))
(defmethod m :after ((obj k2)) (print "after k2"))

(m (make-instance 'k2))
▻ "before k2"
→ k2
▻ "after k2" 

となります。

さて、compute-applicable-methodsで確認してみると、メソッドが増えているのが分かります。

(compute-applicable-methods #'m (list (make-instance 'k2)))(#<standard-method m (:after) (k2) 40E0868EF3>
    #<standard-method m (:before) (k2) 40E086956B>
    #<standard-method m nil (k2) 40E011D08B>
    #<standard-method m nil (k1) 40E004A153>
    #<standard-method m nil (k0) 40E006B64B>)

起動のされ方については、Common LispのMOPで定義されているcompute-effective-methodで起動コードを確認することが可能です。

(c2mop:compute-effective-method #'m
                                (c2mop:find-method-combination #'m 'standard nil)
                                (compute-applicable-methods #'m (list (make-instance 'k2))))(progn
    (call-method #<standard-method m (:before) (k2) 40204EB4DB> nil)
    (multiple-value-prog1
        (call-method #<standard-method m nil (k2) 40E011D08B>
                     (#<standard-method m nil (k1) 40E004A153>
                      #<standard-method m nil (k0) 40E006B64B>))
      (call-method #<standard-method m (:after) (k2) 40204E84E3> nil))) 

上記はLispWorksの例ですが、起動するコードを直接確認できるので、何か良く分からなくなってきたら実際にどんなものが生成されるか確認してみるのも良いでしょう。

このように生成されたコードを眺めてみると、メソッドコンビネーションのコンビネーションの一つとして、フック/Advice機構が実現されていることが分かります。

本体メソッドの前後に副メソッドを配置するコンビネーションは、GoFデザインパターンでもObserverパターンとして良く知られていますが、古えのLisp畑(Flavors等)では、フレーム理論の影響からか、配置されるメソッドをdaemonと呼び、この標準パターンもdaemonと呼んでいます(ややこしい)
伝統を受け継いでいるCommon Lispでもこのコンビネーションが標準となっていて、standardという名前が付いている、という訳です。

フレームワークの代表的な使われ方と、フレームワークそのものが混同されるのは常ですが、メソッドコンビネーションもフックとしての使われ方だけではないことに留意する必要はあるでしょう。

今後は、メソッドコンビネーションの歴史、Common Lispや、Lisp Machine Lisp等でのメソッドコンビネーションの定義方法の解説etcを書いていく予定です(一体書けるのか)


HTML generated by 3bmd in LispWorks 7.0.0

SETFってなに?

Posted 2018-11-30 17:05:36 GMT

Lisp SETF Advent Calendar 2018 1日目 》

Lisp SETF Advent Calendar始まりました。
数年に一度Lisp系でニッチなAdvent Calendarを催したりしていますが、最近あまりニッチなことはしていませんでした。
その反動なのかLisp SETF Advent Calendarですが、ニッチすぎたかもしれません。
ちなみに、過去のニッチなAdvent Calendarには下記のようなものがあります。

SETFってなに?

さて、setfについての簡単な説明です。
プログラミング言語の代入構文にも様々ありますが、左辺に変数名だけでなく、配列の場所であったり、リストであったりが記述できるものがあります。

x := x + 1
a[x] := y + 1
a, b, c := list(0, 1, 2)
...

右側にはどんな値でも置けるのですが、左側に置いて意味を成すものを考えると、値を代入できる場所の指定/参照、となることが多いようです。
Lispのsetfとは、値の読み書きの場所について一般化し、値の読み出しのフォームと同じ見た目のフォームで書き込みも表現しようというものです。

setfは、

(setf 場所 値)

という形式ですが、「場所」には、変数名や値をアクセスする記述を使用できます。
一番身近なリスト操作のcarで説明すると、

リストのcar読み出しフォーム

(setq x (list 0 1 2 3)

(car x) → 0

リストのcar書き込み

(setq x (list 0 1 2 3))(0 1 2 3)

(setf (car x) 42) → 42

x → (42 1 2 3)

となります、Algol風に書けば、

car(x) := list(0, 1, 2, 3)

となるでしょうか。
carで読み出して来た場所に、値を代入するという意図が表現できているかと思います。
リストの場合は、配列アクセスの表現にかなり近いので自然な拡張に思えます。

SETFFが気になる問題

長い歴史を持つLispでは、名前の由来を最早誰もはっきり説明できない関数が結構あります。
実は、setfもそんな名前の一つです。 代入だからsetは良しとして、fはなんだろう、というのはFAQでもありますが、Common Lispのsetfの由来は、L Peter Deutsch氏の論文A LISP machine with very compact programsまで遡ることができます。

代入関数であるsetが変数名だけでなく関数フォームを取れるようにしよう、というのがアイデアの骨子ですが、

setを拡張して、

(set '(fn arg1 ... argn) 42)

と書けるようにし、次に同様にsetq版も考えて、
setqは、set+quoteが由来

(setq (fn arg1 ... argn) 42)

と書けるように拡張。しかし、これでは関数フォーム全体をクォートしてしまい引数が評価されないのが実用上不便ということで、関数名だけクォートするものを考案。

(setfq (fn arg1 ... argn) 42)

この当時のLISP(INTERLISP系)にバッククォートがあれば、

これがCommon Lispでいうsetfと同じもので、(SETFQ (fn argl ... argn) newvalue) which quotes the function name and evaluates everything else.と表現されています。
ということは、setを使えば、

(set (list 'fn arg1 ... argn) 42)

と書けるということでしょうか。
それはさておき、つまりFfunctionを意図していたようですが、一番流布しているのはFField説、次にForm説あたりかなと思います。

ちなみに、やたらとクォートの扱いについて細かいと思う人もいるかと思いますが、L Peter Deutsch氏のバックグラウンドであるINTERLISPでは引数が評価されるものと、されないもの二種(q付きとqなし)が大抵用意されているので、引数評価について敏感だったのでしょう。
Common Lispでは、setsetqの関係ですが、qquoteを意味しているものとしてはsetqが唯一の生き残りです。
なお、setfqqはどっかに飛んでいってしまったようです。

今後は、setfのアイデアが最初に登場したと思われるLISP 2の解説、Lisp Machine Lispでの発展あたりについて書いたり、数あるsetfの定義構文を全部解説してみようかなと思います。


HTML generated by 3bmd in LispWorks 7.0.0

Egison Workshop 2018に参加してきました!

Posted 2018-11-25 08:33:06 GMT

2018-11-23の祝日に開催されたEgison Workshop 2018に参加してきました。

Egisonの現状と今後の展望 - 江木聡志 (楽天技術研究所)

まずは、Egison開発の歴史についての発表でした。
開発動機に始まり現在注力している数式処理機能、テンソルの添字記法等まで解説がありました。
EgisonはあくまでProof of conceptとのことで、今後Haskellへの組み込み機能としても展開していったりするようです。

先日開催されたML Day #2でのEgisonの発表資料

Egison入門+論文紹介 - 西脇友一 (東京大学)

Egisonの入門解説と、来たる12月に開催されるAPLAS 2018で発表予定Non-linear Pattern Matching with Backtracking for Non-free Data Types の解説でした。
まずは、Egisonに慣れてもらうということで、Egisonのパターンマッチの解説が大目で、論文の内容はざっと、という感じでした。

Egisonパターンマッチのアルゴリズムとマッチャー定義 - 江木聡志 (楽天技術研究所)

Egisonのマッチャーのアルゴリズム解説に始まり、無限の大きさの探索空間に対して適切にマッチさせるための工夫のについて解説されました。

こちらも先日開催されたML Day #2でのEgisonの発表資料に詳しいです。

Egisonプログラミングコンテスト

出題をEgisonで解くという内容でした。

member?reverse、というリスト処理では初等的な内容でしたが、Egison流の書法に慣れていない参加者が大半だったため意外と苦戦(私も)しているようでした。

問題集を眺めると、「ポーカーの役判定」までありましたが、そこまでは辿り着かず。
ちゃんと予習していけば良かった……。

Egisonの型システムとその型推論器の実装 - 河田旺 (京都大学)

Egisonに型推論器etcを実装したTyped Egisonを開発している中でのあれこれについてが解説されました。

新機能で知るEgison - 郡茉友子 (東京大学)

テンソルの添字記法に代表される数式処理用の記法を実装するにあたってのあれこれ、EgisonをJupyterで使うためのegison_kernelの紹介、S式がつらいということで、EgisonのHaskell風の構文拡張の紹介でした。

Haskell風のシンタックスについては、egison 4.0に向けての計画の一つなのかもしれませんが、発表内容のものと同一のものなのか、それとは独立の実装なのかメモするのを忘れてしまいました。

現在、対話的に使った際のエラー処理は手厚くないとのことで、新構文で記述→ファイルを読み込み、というフローで使ってみて欲しい、とのことでした。

Lispをやっていて、S式アレルギーの人が一定数いる(大多数)のは十二分に把握しているので、EgisonがS式を採用した理由とメリットについて質問してみましたが、江木さんより、別段にS式に対するこだわりはないという回答でした。
それをうけて私が「S式に対する熱い思いがあるのかと思いました」と発言したところで妙に会場のウケをとってしまい、もしかしたら、Haskell風の構文について質問/紹介するタイミングをつぶしてしまったかもしれません(すいません)

Egisonで微分幾何 - 江木聡志 (楽天技術研究所)

こちらも先日開催されたML Day #2でのEgisonの発表資料に詳しいという感じでした。

懇親会

希望者で懇親会が開催されましたが、参加者間で濃い議論が行なわれていたようです。
私はLisp/S式推しで喋っていましたが、ちょっとウザかったかもしれません(正直すいません)

まとめ

S式構文、パターンマッチ、数式処理、と揃ったので、勝手にREDUCEMACSYMA/Maximaの系譜に続くEgisonキタコレと思ってしまったのですが、主軸はそこではなく、パターンマッチの可能性と記法を追求する途上でたまたまS式、ということのようです。
……ついでにおまけで良いからS式の可能性も追求して欲しい!

全般的にEgison Workshop 2018は、色々な要素のバランスが良く楽しいワークショップでした。

また、参加者にはもれなくEgison Tシャツが配布されました。
Egison Tシャツ欲しかったのでうれしい!

PAP_0016

次回のワークショップも期待しています。


HTML generated by 3bmd in LispWorks 7.0.0

対称性にこだわるGLSと継続と多値

Posted 2018-11-14 20:02:05 GMT

前回、多値についてCommon Lisp以前のLisp Machine Lispから眺めてみました。

色々な資料を眺めている中で、GLS(Guy L. Steel Jr.氏)の多値についてのスタンスが、使い勝手の方に舵を切ったCommon Lispと美しさを取ったSchemeを象徴するように見えたので、GLSと多値について書いてみたいと思います。

タイトルに継続と多値と入っていますが、GLSが多値と継続について語っているのを見付けただけです。継続に興味ある方々には、釣りタイトルみたいになってしまってすいません :)

初期のLisp Machine Lispの多値とGLS

初期のLisp Machine Lispの多値の高レベルAPIにはmultiple-value-listmultiple-value、があり、returnは多値を返せました。
multiple-valueはCommon Lispでは、命名が良くないということで、multiple-value-setqと改名されましたが、当初は代入フォームがメインだったようです。

multiple-value-bindのような束縛構文はというと、調べた限りでは、GLSが提案したものが最初で1977-03-07のメールに詳細が書いてあります。
名前は、Common Lispと同じmultiple-value-bindです。

GLSが提案した形式は、

(MULTIPLE-VALUE-BIND <function call>
                     <bindings>
                     <body>)

というもので、現在のものとは、変数と多値を返すフォームの位置が反対になっています。

この提案では、束縛部ではオプショナル引数や残余引数の指定ができることが示唆されています。

(MULTIPLE-VALUE-BIND (FOO A B C)
        (VAL1 VAL2 &OPTIONAL VAL3) ...)
or
(MULTIPLE-VALUE-BIND (FOO A B C D E)
        (VAL1 VAL2 &REST VAL3) ...)

さらに、入出力の多値の数をしっかり合せたい場合、

(MULTIPLE-VALUE-BIND (BAR B C)
        (G0001 &REST G0002)
        (FOO A G0001 D E))

と書けることを挙げて、

This idea lends a nice symmetry to the passing of arguments and
the returning of values.  Finally, one might note that this also
allows the possibility of specifying that NO values are expected back.
PROG could evaluate statements in this mode, while evaluation
of subforms should require at least one return value.
This lends itself to a better theory of statements vs. forms.

と締めています。
多値の入出力の対称性を実現でき、さらに発展して値を返すフォームを式、返さないフォームを文とできる、ということみたいです。

この提案に反応があったのか無かったのか記録には残っていないのですが、結局この仕様はそのままは取り入れられず、Common Lispでもお馴染の形式のものが1979年に導入されています。

初期のCommon Lisp仕様策定と多値とGLS

時は流れて1981年。初期Common Lispの仕様の議論ですが、当時のmultiple-value-bindの仕様(=現在のCommon Lispの仕様)を少し変更して変数部をlambdaと同じにしたらどうか(つまり上述の1977の仕様と同じ)と提案しています。

I propose that the list of variables be completely identical in syntax
to a LAMBDA-list.  This includes the use of &OPTIONAL and &REST.  One
can get precisely the current functionality by inserting &OPTIONAL at
the front of the variables list.  In addition, one would be able to get
non-NIL default values; a list of some of the values, using &REST;
and better error checking, because one can *require* that certain values
be delivered.

現在のmultiple-value-bindと何が違うかというと、GLSのものは多値の数をきっちり合せることが前提のインターフェイスになっていて、合わなければエラーになるというところです。

(defmacro gls-multiple-value-bind (args mv-form &body body)
  `(multiple-value-call (lambda ,args ,@body) ,mv-form))

(gls-multiple-value-bind (a b) (values 0 1 2) (list a b)) ;error> got 3 args, wanted 2. (gls-multiple-value-bind (a b &optional c) (values 0 1) (list a b c))(0 1 nil)

この提案に対してのDavid Moon氏からの回答が「実用指向のCommon Lisp™」という感じなのですが、実際の所、多値を返す殆どの関数は、それを使う側がいらない多値は捨てるものという前提で運用されていて、理論的に考えられるような対称性はみられない、として切り捨てています。

This sounds superficially plausible and has been proposed many times before.
The reason it has never been accepted is that in practice most functions 
which return multiple values rely on the feature that extra values the
caller does not want are thrown away.  Calling and returning aren't really
as symmetric in practice as they are in theory.  It probably would be useful
to have a way of requiring at least a certain number of values to be
returned.  Of course, then all the functions which rely on extra values
being supplied NIL would have to be fixed.  This is pretty common practice
when you have code like (and <condition> (values <val1> <val2>)).

Schemeと多値と継続とGLS

また時は流れて、1988年、Schemeメーリングリストでのことですが、GLSがSchemeの会合で継続を踏まえた多値の扱いについて話をするつもり、という旨のメールを書いています。

このメールでは、数あるLisp方言で、それぞれ多値の返却/受取で数が合わない場合の対処も違うけれど、継続と多値の問題と見做せるとしています。
また、継続は返却/受取の数は一致していることを基本とし、その上で継続に多値の数が一致しているかを問合せるaccepts?というものを提案しています。

accepts?と使うとCommon Lispの多値のような挙動は下記のように書けるようです。

;;; cwcc = call-with-current-continuation
(define (values . r)
  (define (ignore-excess-values k z)
    (if (accepts? k (length z))
    (apply k z)
    (if (null z)
        (supply-default-falses k r)
        (ignore-excess-values k (cdr z)))))
  (define (supply-default-falses k z)
    (if (accepts? k (length z))
    (apply k z)
    (supply-default-falses k (cons '#f z))))
  (cwcc (lambda (k) (ignore-excess-values k r))))

さらに、beginset!は0個の値を返すのはどうかという話もでてきますが、これも上述の1977年のアイデアですね。

まとめ

多値の返却/受取の個数合せについて、使い勝手の良い挙動でまとめたCommon Lispと、あるべき挙動を選択したScheme、そして時代と方言を越えて一貫した主張をしていたGLSを眺めてみました。

Schemeで多値と継続についての議論がどの辺りからあるのかは調べていないのですが、発表する内容として考えられていたことからしても1988年のGLSの提案が割と初期のものなのではないでしょうか。
ちなみにR7RSでもaccepts?のようなものは仕様には取り入れられていないようです。

Common Lispでは、失敗/成功のフラグをアドホックに多値の二値目で表現するようなことをします。
まさに、多値が導入された当初の目的で使っている感じですが、数合わせしないといけなくなるとすると、ちょっと面倒ですね。


HTML generated by 3bmd in LispWorks 7.0.0

Lispと多値

Posted 2018-11-12 19:54:32 GMT

Goの多値についての記事が人気のようで、この数日Twitterで多値の話題が賑わっています。

多値が話題になることなど、そうそうないですが、多値といえばやっぱりCommon Lispでしょう!、ということでLispと多値について書いてみます。

Lispと多値の歴史

多値といえばCommon Lispですが、最初の仕様のCommon Lisp(1984)でも標準の言語機能になっています。
Common Lispの人達は、普段から普通に便利に使っていますが、多値周りはシンプルなデザインなので使い方で混乱する、ということも特にないでしょう。

典型的な使われ方には下記のようなものがあります。

しかし、その多値機能ですが、Common Lispで初導入という訳ではなく、直接の祖先であるLisp Machine Lispにから輸入したものです。

ということで、Lisp Machine Lispの歴史を遡ってみましたが、私が調べた限りでは、多値が導入されたのは、1976年辺りのMIT LispマシンのCONSのようです。
LISP Machine Progress Report(1977)では、

        A traditional weakness of Lisp has been that functions have to
take a fixed number of arguments.  Various implementations have added
kludges to allow variable numbers of arguments; these, however, tend
either to slow down the function-calling mechanism, even when the
feature is not used, or to force peculiar programming styles. 
Lisp-machine Lisp allows functions to have optional parameters with
automatic user-controlled defaulting to an arbitrary expression in the
case where a corresponding argument is not supplied.  It is also
possible to have a "rest" parameter, which is bound to a list of the
arguments not bound to previous parameters.  This is frequently
important to simplify system programs and their interfaces. 

A similar problem with Lisp function calling occurs when one wants to return more than one value. Traditionally one either returns a list or stores some of the values into global variables. In Lisp machine Lisp, there is a multiple-value-return feature which allows multiple values to be returned without going through either of the above subterfuges.

という風に、入力側のオプショナル引数と、出力側の多値の対で語られています。

複数の値を返したい、というニーズがそんなにあったのかは不明ですが、リストで返したり、大域変数経由で渡すところを多値機構として、すっきり纏めたという話は、それはそれで説得力があるという所でしょうか。

MIT LispマシンはSECDマシンに非常に近い構成らしいので、専用マシンは多値をハードウェア支援で高速に実現できますよ!的な記述も探してみましたが、そういう話はみつけられませんでした。残念。

興味のある方向にCONS時代のLispのマニュアルを置いておきます。

このマニュアルでは、%CALL-MULT-VALUEや、%RETURN-N等のプリミティブが定義されていることが判ります。

ちなみに、Scheme方面では、恐らく1980年代前半にTが最初に導入し、その後紆余曲折を経てR5RSで仕様化されたようです。
Schemeも元を辿れば、Lisp Machine Lisp由来かなと思います。

Common Lispでの多値の扱い

多値の扱いを考える場合、N個の値を返す場合と、受けとる場合にどうなるかを考慮する必要がありますが、Common Lispでは下記のようになります。

値を返す

  1. フォームは0個以上の値を返す

値を受け取る

  1. 関数は単値のみ受けとる
  2. 関数は二つ目以降は無視する
  3. 0個の場合、nilを受け取る
  4. 関数以外のスペシャルオペレーター/標準マクロには個別に規定がある

詳細は、Common Lispの仕様を参照してください。

スペシャルオペレーター/標準マクロの個別の規定ですが、過剰であれば無視され、足りなければ、nilが補われる動作と考えて問題ないと思います(multiple-value-call以外は)。

ただこの挙動をもって、Common Lispの多値機構の挙動と見做せるかというと、そうではなく、そういう風にフォーム構成されているだけという点に注意が必要かなと思います。

受取り側が期待した個数より少なければnilで補填され、敷衍して0個の場合はnilとなるのがCommon Lispの多値機構、という訳ではありません。

例えば、multiple-value-bindは足りない場所はnilを補い、過剰な場合は捨てますが、

(multiple-value-bind (q r s)
                     (floor 1 2)
  (list q r s))
;=> (0 1 nil) 

それは、多値を引数リストにする時にオプショナル引数のように処理しているからで、入出力の多値の個数が一致していなければ、エラーにすることも可能です。
また、nil以外のデフォルト値を設定することも可能でしょう。

(defmacro strict-multiple-value-bind ((&rest vars) mv-form &body body)
  `(multiple-value-call (lambda (,@vars) ,@body) ,mv-form))

(strict-multiple-value-bind (q r s) (floor 1 2) (list q r)) ;error> got 2 args, wanted at least 3.

まとめ

以上ですが、Common Lispでは、継続と多値があまり連続した議論になっていないのがお判りでしょうか。

このまま書き進んで行こうと思いましたが、長くなったので、続きは別の回にしたいと思います。

次回: 対称性にこだわるGLSと継続と多値


HTML generated by 3bmd in LispWorks 7.0.0

NILの商用版が存在していた!

Posted 2018-10-01 17:53:43 GMT

先日 @hatsugai さんからLisp関係の記事があるということで1986年のInformation(インフォメーションサイエンス社)というコンピューター月刊誌をお借りしました。
このInformationですが、1986年は第二次AIブームのまっさなかということで、面白い記事が多かったです。

AIワークステーション花盛りという感じで興味深い広告も多いのですが、その中でも目にとまったのが、裏表紙のMicroVAX IIの広告です。

MicroVAX IIもAI対応してますよ!という広告なのですが、用意されているAI言語として、当時かなりシェアが高かったFranz LispとNILの名があります。
……NIL! NILが商用化されていたとは!
広告の文言をそのまま引用すると、

■NIL
MIT AI Lab. で開発されたCommon LISP
米国Impediment(製)

とのことで、Impediment社があつかっているということですが、LispはSpeech Impedimentみたいなので、そんな洒落を社名にするならLisp関係の会社でしょうか。
早速ネットを検索してみたりしていたのですが、Impediment inc という検索ワードで、それらしき情報が出てきました。

この記事によると、マシンスペック 5-16メガRAMのVAXで稼動し、Fravorsによるウィンドウシステム、Emacs系エディタが開発環境として用意されていますようです。

Fravorsによるウィンドウシステムは、ウィンドウシステムまるごとなのかVAXのものにのっかったものなのか知りたいところです(当時だと丸ごと作っていそう)が、Lispマシンのものを移植したりしていそうです。

Emacs系エディタについては、NILのマニュアルではSteveというものが標準エディタとして解説されていますので、多分Steveが動いたのではないでしょうか。 ちなみに、Steveの他にNileというものがあったとWikipediaに記載がありますが、実在したのかは未詳。

Impediment社は90年代からウェブサイトを所有しているようで、現在もドメインは活きているようですが、ウェブサイト自体は落ちているようです。 会社の住所は1985年の記事のものと同じようなので同一の会社かと思われます。

whoisで所有者情報を確認してみたところ、Alexander Sunguroff氏が管理者の様子。過去のAI関係の記事に社名と一緒に紹介されているようなので経営者のようにみえます。

Sunguroff氏は、1970年代初頭にはProject MACに関わっていたようで、主に初期のMultics MACLISPの開発にDavid Moon、….氏と共に携わっていたようです。

また、MACLISPマニュアルの定番である1974年版はDavid Moon氏とSunguroff誌が一緒にまとめたようです。

NILは、MITでのLisp Machineプロジェクトと同じく、MACLISPの後継ですが、後にCommon Lispの礎となり、自らもCommon Lispのスーパーセットとなりました。 Common Lispがレキシカルスコープを採用したのは直接的にはNILの影響だといわれています(当事者達曰く)

この辺りの経緯を鑑みるに、筋金入りのLisperであるSunguroff氏が1984年あたりに宙ぶらりんになっていたNILを商用化するために会社を興してもおかしくはないなと推理しているのですが、真相やいかに。

当時のLisp環境の最高峰といえば、Lispマシンでしたが、汎用機上のライバル達もLispマシンに追い付け追い越せで、Lisp OS、ウィンドウシステム完備のGUI、Lisp向けエディタ(Emacs etc)を一式揃えて勝負していましたので、NILもOSこそLisp OSではないものの、そんな感じだったのではないかと思います。

NILはMIT内で利用されていたのみと思っていたのですが、製品として世に流通していたとすれば、そのうち誰かが発掘してbitsaversにでも置いてくれるかもしれません。
いつかNILを起動してみたい……。


HTML generated by 3bmd in LispWorks 7.0.0

SBCL 1.4.12のfold-identical-codeを試してみた

Posted 2018-09-29 19:35:02 GMT

今月も月末にSBCLの新バージョンがリリースされましたが、リリースノートに、

  • SBCL All News: 1.4.12
    enhancement: identical code (at the machine instruction level) can now be shared between functions, if explicitly requested.

とあり、どんな機能か気になったので調べてみました。

fold-identical-code

ちょっと調べても説明もなく良く分からなかったので、githubのコミットログを眺めましたが、fold-identical-codeとかいうのが、その機能のようです。

fold-identical-code を使ってみる

どう使うのかは、良く分かりませんが、

fold-identical-codeが定義されているsrc/code/icf.lisp

;;;; Identical Code Folding (similar to what might be done by a C linker)

とあるのでコードサイズの縮小あたりが狙いなのかもしれません。

とりあえず、下記のような同じ内容の関数を定義してみてからfold-identical-codeを実行してみると、

(progn
  (defun foo (x) (+ 42 x))
  (defun bar (x) (+ 42 x))
  (defun baz (x) (+ 42 x)))

(fold-identical-code :aggressive t :print t)

以下のようにずらっと結果が表示されますが、

#<code id=154F2 [1] baz {53FD93CF}> = #<code id=154F0 [1] foo {53FD92AF}>
#<code id=154F1 [1] bar {53FD933F}> = #<code id=154F0 [1] foo {53FD92AF}>

barbazfooにまとめられているようです。

使用メモリの削減具合を確認するためにイメージをダンプしてみましたが、135MiBのイメージが、ダンプ時にfold-identical-codeを実行してからダンプすると、134MiBに縮みました。

1%も縮まっていない感じですが、C++等のリンカの最適化を解説しているページによると、重複の削除によって1〜2%縮むと書いてあるものが多いようなので、そんなものなのでしょう。

むすび

今の所ドキュメントは整備されておらず、src/code/icf.lispsrc/code/icf.lispを読むしかない感じですが、今後整備されていくと思うので期待して待ちたいです。

ちなみに当初自分が期待していたのは、disassembleした時のインストラクションが同一になるかどうか確かめるユーティリティだったのですが、下記のようにfold-identical-codeの部品であるsb-vm::code-equivalent-pを使って判定できるようです。

(defun fun-code-equivalent-p (f g)
  (sb-vm::code-equivalent-p (list (sb-c::fun-code-header f))
                            (list (sb-c::fun-code-header g))))

(fun-code-equivalent-p #'foo #'bar) ;=> t

ごくたまに欲しい時があって、disassemble関数の中身を使って自作していましたが、sb-vm::code-equivalent-pを使った方が正確に判定できるようなので、今後はこちらを使おうかなと思っています。


HTML generated by 3bmd in LispWorks 7.0.0

Shibuya.lisp 発足十周年おめでとう

Posted 2018-09-17 16:46:09 GMT

いきなりですが……、Shibuya.lisp 発足は十年前の2008-09-18だと思っていましたが、2008-09-08でした……。
日付間違えて覚えてて10周年過ぎてました。なんてこったい。

自分は、発足当初のスタッフで途中で交代した人間ですが、10年も続いているのは素晴しいなと思います。

ざっとこの十年を振り返ってみると、テクニカルトークと、Lisp Meet Upがあります。
最近は、もくもく会というものあるようです。

  • 2008-10-18 Shibuya.lisp テクニカルトーク#1
  • 2009-02-28 Shibuya.lisp テクニカルトーク#2
  • 2009-07-04 Shibuya.lisp テクニカルトーク#3
  • 2009-11-07 Shibuya.lisp テクニカルトーク#4
  • 2010-03-20 Shibuya.lisp テクニカルトーク#5
  • 2010-11-27 Shibuya.lisp テクニカルトーク#6
  • 2011-09-26 Shibuya.lisp テクニカルトーク#7
  • 2014-08-30 Shibuya.lisp テクニカルトーク#8
  • 2013-01-22/2018-09-27 Lisp Meet Up presented by Shibuya.lisp #1/#68

Lisp Meet Upの方は、今月開催分で実に68回目。
Lisp Meet Upは、なわたさんを中心として企画され始まったと記憶していますが、Meet Upがなければ2012年あたりで実質消滅していたかもしれないなと思います。

テクニカルトークの方は、大体50から100人程度の参加者で開催していたものですが、開催の負担が大きかったこともあり、2014年以降は開催されていません。
まあでも最近だと100名程度の参加者であれば手軽に開催できるイベント会場があるようなので、発表さえ揃えられれば開催できるのかもしれません。

この十年の傾向

2008年頃は、勉強会ブームがあり、また言語を学ぼうというブームがありました。
Lisp系言語では日本のSchemeの代表格であるGaucheが圧倒的だったと記憶しています。
一方、Common Lispグループは割と草の根的に活動していき、地味にネットワークを広げていったようです。

言語学習ブームが去りつつあった2011年あたりから、Schemeでなにか発表しようという人は少なくなる一方で、Clojureが擡頭してきて、Lisp Meet Upは、Common LispとClojureを中心にして今に至るようです。
Schemeは端から眺める限りでは、Schemer=処理系開発者という感じなのでユーザーが集まって何かするより、開発者間で議論して個人間で盛り上がるという感じに見えたので、言語がコミュニティを形成させる上での形質の違いのようなものがあるのかもしれません。

現在では、業務でCommon Lisp、Clojureを書く人材/職場の交流もShibuya.lispをきっかけにして、ということも多々あるようです。

まとめ

次の十年の継続と発展を期待しています!


HTML generated by 3bmd in LispWorks 7.0.0

boundpがGeneralized Booleanを返す謎

Posted 2018-08-29 09:56:05 GMT

Common Lisp仕様策定のメーリングリストで、RMSが()NILは区別すべき、という主張をしていて、そうした場合の御利益について説明しているのをみつけたので眺めていました。

NIL()を区別すべきという主張はさておき、文末の方にCommon LispでいうGeneralized Booleanのアイデアが紹介されています。

RMS:
The general principle is: if a predicate FOO-P is true if given falsehood as an argument, FOO-P should always return an object of which FOO-P is true. If, on the other hand, FOO-P is false when given falsehood as an argument, then FOO-P should always return its argument to indicate truth.

foo-pが真の時は、述語の与えられたオブジェクトを返すということの一般化ですが、Common LispでのGeneralized Booleanは、ざっとHyperSpecを数えたところ99個あるようです。

next-method-p remf slot-exists-p random-state-p simple-string-p
wild-pathname-p compiled-function-p slot-boundp graphic-char-p arrayp
string= string-equal simple-bit-vector-p array-in-bounds-p functionp
fboundp hash-table-p tree-equal symbolp listen input-stream-p
output-stream-p listp readtablep = /= < > <= >= pathname-match-p atom
streamp delete-package eq boundp keywordp logtest remprop characterp
open-stream-p every notevery notany unintern pathnamep complexp
alphanumericp realp simple-vector-p y-or-n-p yes-or-no-p eql vectorp
endp equal stringp remhash interactive-stream-p packagep evenp oddp
bit-vector-p subsetp alpha-char-p load minusp plusp numberp typep
adjustable-array-p zerop standard-char-p logbitp constantp tailp
special-operator-p rationalp array-has-fill-pointer-p char= char/=
char< char> char<= char>= char-equal char-not-equal char-lessp
char-greaterp char-not-greaterp char-not-lessp upper-case-p
lower-case-p both-case-p ldb-test fresh-line integerp equalp consp

boundpの謎

Generalized Booleanとして機能した場合に返す値が謎なものは結構ありますが、中でも、boundpが気になります。

cl:nilは偽値であるcl:nilに束縛されているので、(boundp 'cl:nil) => Tと真値を返すわけですが、これをGeneralized Boolean化した場合、(boundp 'foo)fooシンボルを返すのは良いとして、cl:nilシンボルでcl:nilを返したら偽になってしまいます。

上記のRMSのメールでは、こういう場合は、Tを返すようなことを書いていますが、Generalized Booleanとしての機能は果すものの、今度は、(boundp 'cl:NIL)(boundp 'cl:T)を区別できなくなります。

まとめ

Generalized Booleanを返しても良いもののうち、実際の利用例については首をひねるものは、boundp以外にも結構ありますので、暇潰しに考察するのも一興かなと思います。


HTML generated by 3bmd in LispWorks 7.0.0

色々な言語でCommon Lispのコードを書いてみよう

Posted 2018-08-27 17:23:54 GMT

色々な言語でCommon Lispのコードを書いてみたら、どうかなと思い試してみました。
だからなんなの、というような試みです。

コード例はお馴染みのfibにします。

Common Lisp

大元のCommon Lispです。
quoteを付ければデータになりますが、一応listでリストを生成するものも書いてみます。

'(defun fib (n)
   (if (< n 2)
       n
       (+ (fib (1- n))
          (fib (- n 2)))))

(list 'defun
      'fib
      (list 'n)
      (list 'if
            (list '< 'n '2)
            'n
            (list '+ 
                  (list 'fib (list '1- 'n))
                  (list 'fib (list '- 'n 2)))))

当然ながらevalすれば、コードとして実行できます。

Racket

まあ、RacketもCommon Lispもそのまんまですね。
1-というシンボルは関数名としては不可だった気もします。

'(defun fib (n) (if (< n 2) n (+ (fib (1- n)) (fib (- n 2)))))

defun等々の関数を定義すれば、そのまま動かせると思います。

Python

PythonではCLのリストに相当するものは配列になるでしょう。
ASTやシンボルへのアクセスもできるようですが、とりあえず、面倒なのでシンボルは文字列としておきます。

['defun',
 'fib',
 ['n'],
 ['if',
  ['<', 'n', 2],
  'n',
  ['+', ['fib', ['1-', 'n']], ['fib', ['-', 'n', 2]]]]]

上のコードを動かすにはインタプリタを作成する感じなんでしょうか。

Ruby

RubyもPythonと似た感じですが、シンボルがあるので使ってみました。

[:defun,
 :fib,
 [:n],
 [:if, [:<, :n, 2], :n, [:+, [:fib, [:"1-", :n]], [:fib, [:-, :n, 2]]]]]

Erlang

何故Erlangという気もしますが、Prologっぽいので試してみました。
やはりシンボルがあるので扱いが楽です。

[defun,fib,[n],['if',['<',n,2],n,['+',[fib,['1-',n]],[fib,['-',n,2]]]]]

Prolog

Prologは、Lispと同様、基本のデータ型がリストです。Lispのシンボルはアトムに相当するでしょうか。

[defun,fib,[n],[if,[<,n,2],n,[+,[fib,[1-,n]],[fib,[-,n,2]]]]]

Julia

最近ブレイクしつつあるような気がするJuliaですが、シンボルがあるので、そこそこ素直に書けます。

[:defun, :fib, [:n], [:if, [:<, :n, 2], :n, [:+, [:fib, [Symbol("1-"), :n]], [:fib, [:-, :n, 2]]]]]

データをコードにする仕組みが割と身近に存在するようで、文字列をパーズしてExpr型というコードに変換することが可能です。

julia> xpr="function fib(n)
         if n < 2
            n
          else
            fib(n - 1) + fib(n - 2)
         end
       end"

julia> Meta.parse(xpr) :(function fib(n) #= none:2 =# if n < 2 #= none:3 =# n else #= none:5 =# fib(n - 1) + fib(n - 2) end end)

julia> Meta.show_sexpr(Meta.parse(xpr)) (:function, (:call, :fib, :n), (:block, :(#= none:2 =#), (:if, (:call, :<, :n, 2), (:block, :(#= none:3 =#), :n ), (:block, :(#= none:5 =#), (:call, :+, (:call, :fib, (:call, :-, :n, 1)), (:call, :fib, (:call, :-, :n, 2))) )) ))

コードをExprで直接記述することもできるようで、この辺りは、まんまLispの前置記法ですね。

Expr(:function, Expr(:call, :fib, :n), 
     Expr(:if, Expr(:call, :<, :n, 2), 
             :n,
             Expr(:call, :+, 
                    Expr(:call, :fib, Expr(:call, :-, :n, 1)), 
                    Expr(:call, :fib, Expr(:call, :-, :n, 2)))))

(追記) Squeak/Pharo Smalltalk

ブログの内容が謎コンテンツでしたが、意外にもSmalltalk方面から反応を頂きました。
ありがとうございます🙇

シンボルがあるSmalltalkもかなりLispっぽく書けますね。しかも丸括弧。

まとめ

Common Lispは、プログラムを文字のつづりというよりは、言語処理系のリスト型とシンボルで記述しますが、他の言語のデータ型を使ってCommon Lispのコードを書いてみたら面白いかなと思って試してみました。

当然ながら、やっぱり、Lisp、Prologあたりは、この辺りの処理はしやすいですね。

Juliaは数値計算にフォーカスしているものと思っていましたが、案外Lisp的なコード=データな文化も継承しているようで、なかなか面白いと思いました。


HTML generated by 3bmd in LispWorks 7.0.0

キーワード引数のキーワードを定数として宣言してつかう

Posted 2018-07-01 15:01:17 GMT

三年以上寝かせたアイデアですが、まったく有用なケースを見出せていません。

(defconstant ? :test)

(member "a" '("b" "c" "a") ? #'string=)("a")

何か活用法あるでしょうか。

ちなみに、いまのところ有用なケースは見出せていませんが、マクロやコンパイラマクロの引数定義をめんどくさくしたりコンパイラの最適化を阻害する欠点はあるなとは思いました。


HTML generated by 3bmd in LispWorks 7.0.0

MACLISPでアラビア数字・ローマ数字変換

Posted 2018-06-20 14:39:53 GMT

最近、過去のブログ記事を眺めかえしたりしているのですが、shiroさんのブログに、アラビア数字・ローマ数字変換ネタがあり、

とありました。

私は、MACLISPは対称の筈だ!と思いMACLISP用のコードを書き始めましたが、どうも昔に書いたことがあると思いローカルのファイルを検索してみると、どうやら記事を読んだ当時に書いていたようで、ついでにGitHubにコードもアップしてました。

これが可能なのはMACLISPは基数にローマ数字を指定できるからなのですが、詳細は関連記事の動画を参照してください。
どうして当時記事にしなかったのだろう。マニアックすぎたからだろうか……。

関連記事


HTML generated by 3bmd in LispWorks 7.0.0

C++のstd::find_ifの名前の由来はCommon Lispのfind-ifだった

Posted 2018-06-17 17:40:10 GMT

表題の通りで、だからなんだよといわれればそうなのですが、以前、C++ STLにCommon Lispの影響はあるのかを探る旅に出ていました。

この記事の結論としては、明言はされていないものの、Common Lispのシークエンス関数をごっそり取り入れているようにしか見えないので、Common Lispが元ネタはじゃないだろうか、という感じでしたが、先日Stepanov先生の「その数式、プログラムできますか?」を読んでいたところ、find_ifの注釈として脚注に、

"find it if it's there"(あれば検出する)の意。この名前の起源はCommon Lispプログラミング言語にある。
                                                         ─第10章 プログラミングの基礎概念 P216

と明言されていました。
まあ名前どころじゃなくて、シークエンス系ユーティリティの概念とか挙動は、結構な数が取り込まれていると思いますので、興味のある方はチェックしてみてください。


HTML generated by 3bmd in LispWorks 7.0.0

勝手に「日本LispWorksユーザー会(非公式)」を作りました

Posted 2018-05-26 17:39:01 GMT

私の身近にLispWorksを使う人がちらほら増えてきているのですが、バグやパッチの情報だったり、便利な使い方だったりの情報共有をもっと活発にしたいなと思い、Google Groupsで勝手に「日本LispWorksユーザー会(非公式)」というのを作成しました。

主な用途・ターゲットユーザー

  1. LispWorksのパッチやバグの情報共有
  2. LispWorks全般、特に日本語処理周りについての情報共有

が主な目的で、その辺りに興味がある方々がターゲットユーザーです

まあ、Lisp Hugメーリングリストで尋ねてみたら?というアドバイスが殆どになってしまうかもしれませんが、それはそれで良いかなと思っています😃


HTML generated by 3bmd in LispWorks 7.0.0

an obscure feature of MacLISP

Posted 2018-05-13 13:53:03 GMT

小学館ランダムハウス英和大辞典のオンライン版で、“obscure”の用例に何故かMACLISPが出てきているのを発見しました。

goo 英和・和英辞書 「an obscure feature of MacLISP」の意味

an obscure feature of MacLISP

説明書に書かれていない MacLISP (コンピュータ言語)の一特徴.

ちゃんとMITっぽい言い回しですが、一体どこから紛れ込んだのでしょう……。


HTML generated by 3bmd in LispWorks 7.0.0

Lispに有理数(分数)が導入されたのはいつ頃なのか

Posted 2018-05-09 15:03:26 GMT

turingcomplete.fm #17 を聴いていたら、Lispと有理数について、またそれはいつ頃導入されたのかという話題がでてきました。
そういえばLispが有理数をサポートしたのっていつ頃なのか私も知らないな、ということでちょっと調べてみましたが、1981年あたりが境ではないかと推測できたものの、結局あまりはっきりしたことは分からず。
とはいえ、折角調べたのでまとめて記事にしてみることにしました。

Common Lisp

しかし、とりあえず「Lispは数値計算周りの仕様が充実している」という評判はCommon Lisp(1984)辺りが始まりのようです。
次にSchemeが、RRRS(1985)でCommon Lisp(1984)の成果を評価し、それを取り入れたようですが、その際にCommon Lispでは定義されていない正確数と非正確数の概念ができたようです。
当時の背景があまり想像できませんが、移植性のある正確数とそうでないものに分けたのでしょうか。
RRRSではCommon Lispより前には体系的に数値計算をまとめたLispはなかったと述べられているので、Common Lispが一つの境なのでしょう。

S-1 Lisp

では、そのCommon Lispは、どこから有理数を取り入れたかというと、当時の科学計算用スーパーコンピュータ用LispであるS-1 Lisp(S-1 NIL)からのようで、取り入れる方針が決まったのは1981年位のことのようです。

  • The Evolution of Lisp:

    Fancy floating point numbers, including complex and rational numbers (this was the primary influence of S-1 Lisp)

S-1 LispはS-1 NILとも呼ばれるように、方言としてはNILであり、Common Lispが制定された頃にはNILはCommon Lispになってしまっているので、どこからNILでどこからCommon Lispかは分かりませんが、数値計算回りでのS-1 Lispの影響は、かなり大きそうです。
S-1 Lispでは、数値計算回りでハードウェアの支援が手厚く、有理数のサポートでもハードウェアの支援を受けることができていたようです。

Lisp Machine Lisp

また、同時期の1981年後半には、S-1 Lispとは独立してLisp Machine Lisp(Zetalisp)(CADR System 74)にも導入されていますが、製品として世の中に出たLispとしては最初のものではないでしょうか(Symbolics/LMIから製品化)。
ちなみに、Lisp Machine Lispの方の有理数は、当初は、エスケープ文字が/なので、3\10のように記述されていましたが後のCommon Lisp化で3/10のように表記されるようになったようです。
また、Lisp Machine Lispでは、Common Lispのように整数との統合はされていないようで1\11は別物になるようです。

1981年のLispコミュニティ

Lispコミュニティでは、1981年辺りで有理数の議論がよくされていたようで、1981年あたりに何らかの機運があったのかなと思います。

Lisp、Smalltalkの双方で活躍していたL Peter Deutsch氏がSmalltalk-80には有理数サポートがあるという投稿をしていますが、こういう発言があるということはやはり1981年辺りで有理数をサポートしたメジャーなLisp処理系はなかったのではないかと思います。


HTML generated by 3bmd in LispWorks 7.0.0

いつのまにやらArc公開十周年

Posted 2018-04-15 19:31:36 GMT

すっかり忘れてしまっていましたが、「百年の言語」を体現するarcが公開されたのは、十年前の2008-01-29のことでした。
本当は、今年の01-29に何か書こうと思っていましたが、それさえ忘れて早二ヶ月半……。

arcのソースコードが公開されたのは2008年でしたが、arcで構築されたサイトのHacker Newsは、2007年から稼動しています。
また、名前と構想が発表されたのは、2001年のことなので、かれこれ十八年ともいえなくもないです。

最近のarc

arcの近況を眺めてみましたが、オフィシャルなリリースは、2009-08の3.1から9年間動きなしです。
Hacker Newsを動かしていたのはarcでしたが、2014年にpg氏がY Combinatorの日常業務から引退し、今ではまだarcで動いているのかは良く分からないようです。

軒並動きはないのかと思いましたが、arcのコミュニティが開発しているAnarkiの方は最近も更新があるようです。
Racketに#lang anarkiを作成するなど、なかなか面白そう。

早速、試してみましたが、Racketがインストールされた環境であれば、

raco pkg install anarki

とするだけで導入されます。

あとは、こんな感じで記述しRacketから使えます。 (無理にarcっぽさを出してみました。)

#lang anarki

;;; fib.rkt

(:provide fib)

(def fib (n) (if (< n 2) n (+ (fib:- n 2) (fib:- n 1))))

;; Racket
(require "fib.rtk")

(fib 40) ;→ 102334155

むすび

もっと色々書こうと思いましたが、昔の文献を読んでいたらお腹一杯になって満足しました。

参考

このブログのarc関係の記事


HTML generated by 3bmd in LispWorks 7.0.0

defvar で値を再設定したい時

Posted 2018-04-10 22:09:49 GMT

defvarには値が設定されている場合には再設定をしないという利点がありますが、開発中などはdefvarの値を再設定したい場合がままあります。

こんな時の為に便利マクロを定義するという記事を読みました。

目立たない機能なのであまり知られていないですが、Emacs系のLispエディタ上ではdefvarのフォームを評価したら値が再設定される、という機能があります。
どうやらdefvarが誕生した(1979年頃)からある機能の様子。
(多分、defvar誕生の頃から扱いがめんどくさいという認識があったのでしょう……)

Zmacs(ZWEI)だと、COM-EVALUATE-REGION-HACKというのがあって、defvarsetqに置き換えて評価するようです。

slimeだとslime-eval-defunで評価すれば、defvarを認識して下請けのslime-re-evaluate-defvarで値を再設定します。
再設定の方法は、一度makunboundしています。

LispWorksだと、“Evaluate Defun”が“Reevaluate Defvar”を呼んで再設定します。slimeと同じくmakunboundするようです。
エディタ変数editor::evaluate-defvar-actionの値が、:reevaluate-and-warn:reevaluateの場合だけこの動作になるので好みで入切可能です(デフォルトは有効)

Hemlockだと、“Re-evaluate Defvar” コマンドのみで、“Evaluate Defun”とは連動しないようです。こちらもmakunboundして再設定。

新しい値でsetqするものかと思っていましたが、案外makunboundで処理する方式の方が多いみたいですね。

むすび

“Reevaluate Defvar”は、リージョンを評価した時などにも発動していることがあります。
大抵の開発時は、再評価してもらった方が嬉しいですが、defvarの値設定まわりがどうも不思議な挙動だなと思ったら、確認してみるのも良いかと思います。

関連


HTML generated by 3bmd in LispWorks 7.0.0

CONSマシン時代のLisp Machine Lispのマニュアル

Posted 2018-04-04 19:46:48 GMT

MIT Lispマシンのソースディレクトリを漁っていたら、MITの最初のLispマシンであるCONS時代のLisp Machine Lispのマニュアルらしきものをみつけたので、html化してみました。

日付は、1977-06-08ですが、CONS時代のドキュメントであるAIM-444: LISP Machine Progress Reportと照らし合わせてもCONS時代で間違いないようです。

AIM-444の最後に書いてあるように、プロトタイプであるCONSは、1977年に一応の完成となり、1978年あたりから実用を目的としたCADRの開発が始まります。

CONS時代のLisp Machine Lispで興味深い所

EVERYSOMEはINTERLISPから輸入してきたものであることが書かれていたりします。

また、CONS時代にはまだ、パッケージがないのですが、どうやら接頭辞で管理していたようです。
雰囲気としては今のEmacs Lispに近いですが、これらは、PREFIX REGISTRYのまとまりとして何らかのお作法があった様子。

現在のCommon Lispにも存在するread-from-stringが既にありますが、read-は接頭辞だったようなので、当初は、Common Lispでいうとread:from-stringみたいな感じだったのかもしれません。
しかし、これらの接頭辞は、その後パッケージができても名前はそのまま変更されることもなく後の慣習と混ざり、なんとなく命名規約が二重になった感じを残したようです。

また、現在のLispでは単語を-で繋ぎますが、一部_で繋いだものも存在したようです。これらは後に-で統一されます。

%は現在の命名慣習でも公でない内部関数を表わしたりしますが、初期のLisp Machine Lispでもそのようです。
この後、CADR時代になってくると、マイクロコードで書かれた低レイヤーのサブプリミティブで値を返すことが前提になっていないものに%が付くという慣習になって行ったようです。

%が重なる%%の命名規約は謎です。安直に考えると、さらに内部の関数という感じですが、どうもそうでもない雰囲気があります。

面白いのが、下請けを意味する*と併用されたりすることで、例えば、%*NCONCは、可変長のNCONCの二引数版でかつ公でない関数、という感じになります。また、%*GENSYM-COUNTERのように変数に適用されることもあるようです。

TRACE-
GRIND-
TV-
PC-PPR-
KBD-
FILE-
PRINT-
READ-
FASL-
ED-
ED-COM-
%
%%
*
%*
DTP-
%%Q-
CDR-
%%AREA-MODE-
FSM-
%SYS-COM-
ADI-
%%ADI-
LP-
%%LP-
%%ARG-DESC-
%FEF-
%%FEF-
FEF-
%%ARRAY-
%ARRAY-
ARRAY-
ART-
FEFH-
%FEFH-
%%FEFH-
FEFHI-
%%FEFHI-
%%PHT1-
%%PHT2-
%PHT-
SG-
%%SG-
MESA-
%%MESA-

むすび

CONS時代のLispがどのようなものだったかの資料は少なく、AIM-444: LISP Machine Progress Reportに書いてあることも謎が多かったのですが、このマニュアルで若干謎が解明されました。
また、Lisp Machine LispはCADRから始まるような印象がありましたが、初期のものを含めると1975、6年あたりから存在するといえそうです。


HTML generated by 3bmd in LispWorks 7.0.0

マストドンにreddit r/lisp r/lisp_jaの新着ボットをつくりました

Posted 2018-04-02 16:51:52 GMT

Lispな人でマストドンを利用している人がどれだけいるのかは分からないのですが、ボットを作成するのが簡単だったので、Twitterボットが投稿するついでにマストドンにも投稿するようにしてみました。

r/lisp_jaと、その他英語Lisp系サブレディットの詰め合わせ版の二種があります。

TLのお供にどうぞ。

ちなみに、Twitterはこちらです。


HTML generated by 3bmd in LispWorks 7.0.0

Common LispのLOOPやFORMATを真似した例は割とある

Posted 2018-03-28 07:45:44 GMT

実際の所、LOOPやFORMATはCommon Lispを使ったことがない人達にも複雑さの象徴のように語られていますが、割合にCommon Lisp以外にも輸出されています。
どちらもライブラリレベルの機能なので導入も難しくはありません。

言語仕様に入っているかどうか、ということならば、いまどきはライブラリレベルで実現できるものは言語のコアに入れず、標準ライブラリ位にまとめる所をCommon Lispでは全部標準仕様内にまとまった構成になっている、という点が特徴ですが、言語設計でそういう選択をしたので、こうなっています。
また、LOOPやFORMATはCommon Lisp以前から存在しますが、Common Lispより前のLispではライブラリとして導入されています。

コアに入っているかどうかということになると、LOOPはPython等の内包表記と機能的にはほぼ変わらないもので、ある意味先祖的なものに感じます。
内包表記の難解さに対する批評がありますがLOOPへの批評も似たものがあります(前置 vs 中置の議論の他に)

FORMATも繰り返しがあったりする所が珍しいですが、繰り返し以外では、いまどきの言語に似たようなものは多数ありますし、より多機能なものも多いです。

ちなみに、LOOPやFORMATよりいまどきの言語が標準搭載している正規表現エンジンの方が複雑だと思いますが、正規表現エンジンはCommon Lispでは標準規格にはないので、みなライブラリを利用しています。

ライブラリレベルで移植されたLOOPやFORMAT

ライブラリレベルで移植された例をざっと挙げてみましょう。

LOOP

INTERLISPのclispという環境があり、そこで中置的に書ける構文のforがMACLISP系Lispに取り入れられました。 主にMIT Lispマシングループで良く使われ、今に至ります。

Common LispのLOOPでできることは殆どINTERLISPのforで可能なのでLOOP自体が「誰かが真似したもの」でした

Common Lisp以外に移植された例

Scheme

Emacs Lisp

Clojure

等々、多数あります。

全部乗せな繰り返し構文というLOOPと似たコンセプトのものだとsrfi-42を筆頭に多数あります。

また、Common Lisp内でもLOOP代替構文というのは多数あります。

FORMAT

FORMATの出自

FORTRANのFORMATをMACLISPに導入したのが始まりのようです。

Clojure

Scheme

Emacs Lisp

全部乗せな出力構文というFORMATと似たコンセプトのものだとfmtを筆頭に結構あります。

むすび

実際の所、FORMATやLOOPは大したものでもなく、フルスペックでも大体1000行前後の実装が多いので、ひまつぶしのログラミングの題材には丁度良いと思います。


HTML generated by 3bmd in LispWorks 7.0.0

「Generatorの勧め」をCommon Lispで

Posted 2018-03-25 09:11:27 GMT

ClojureにTransducers、SchemeにGenerator(SRFI-158/SRFI-121)だそうですが、Common Lispだったらseriesでしょう、ということでCommon Lisp版を書いてみました。

target1が「リスト生成→フィルタリング」、target2が「ジェネレータを使ってストリーム的に処理」するので中間コンスが少なくできる、という内容です。

今回の場合は、そっくりそのままCommon Lisp+seriesで写しとれる感じです。

下準備

;;; scheme
(define size 1000)

;;; cl
(ql:quickload :series)

(defpackage gen (:use :cl :series))

(in-package :gen)

(defconstant size 1000)

target1

;;; scheme
(define (target1)
  (filter (lambda (x) (zero? (mod x 3)))
          (map cdr
               (filter (lambda (x) (odd? (car x)))
                       (map (lambda (x) (cons x (square x)))
                            (iota size))))))

;;; cl
(defun target1 ()
  (remove-if-not (lambda (x) (zerop (mod x 3)))
                 (mapcar #'cdr
                         (remove-if-not (lambda (x) (oddp (car x)))
                                        (mapcar (lambda (x) (cons x (expt x 2)))
                                                (loop :for i :from 0 :repeat size :collect i))))))

target2

(define (target2)
  (generator->list
   (gfilter (lambda (x) (zero? (mod x 3)))
            (gmap cdr
                  (gfilter (lambda (x) (odd? (car x)))
                           (gmap (lambda (x) (cons x (square x)))
                                 (make-iota-generator size)))))))

(defun target2 ()
  (collect 
   (choose-if (lambda (x) (zerop (mod x 3)))
              (map-fn t
                      #'cdr
                      (choose-if (lambda (x) (oddp (car x)))
                                 (map-fn t
                                         (lambda (x) (cons x (expt x 2)))
                                         (scan-range :length size)))))))

target2seriesのリーダーマクロを使えばこんな感じにも書けます。
seriesを使うならこっちが普通かもしれません。

(series::install)

(defun target2/ ()
  (collect 
   (choose-if (lambda (x) (zerop (mod x 3)))
              (#Mcdr (choose-if (lambda (x) (oddp (car x)))
                                (#M(lambda (x) (cons x (expt x 2)))
                                   (scan-range :length size)))))))

implicit-mapを有効にすれば、更に簡潔に書けますが、ソースの字面で若干混乱しそうになるのでお勧めはしません :)

(series::install :implicit-map T)

(defun target2// ()
  (collect 
   (choose-if (lambda (x) (zerop (mod x 3)))
              (cdr (choose-if (lambda (x) (oddp (car x)))
                              ((lambda (x) (cons x (expt x 2)))
                               (scan-range :length size)))))))

target3

ついでにdo職人による極力コンスを排したコードも参加してみます。

(defun target3 ()
  (do* ((x 0 (1+ x))
        (y (expt x 2) (expt x 2))
        (ans (list nil))
        (tem ans))
       ((= size x)
        (cdr ans))
    (when (and (oddp x) (zerop (mod y 3)))
      (setf (cdr tem)
            (setq tem (list y))))))

比較結果

今回はタイムというより無駄なコンスを減らすことができるかがポイントのようです。
なお回数は一万回に増やしてみました。

(defparameter *count* 10000)

(equal (target1) (target2)) → T

(time (dotimes (i *count*) (target1))) #|| Timing the evaluation of (dotimes (i *count*) (target1))

User time = 0.521 System time = 0.000 Elapsed time = 0.508 Allocation = 678292704 bytes 0 Page faults Calls to %EVAL 160036 ||#

(time (dotimes (i *count*) (target2)))

#|| Timing the evaluation of (dotimes (i *count*) (target2))

User time = 0.236 System time = 0.000 Elapsed time = 0.226 Allocation = 198294392 bytes 0 Page faults Calls to %EVAL 160036 ||#

seriesを使ったtarget2方がコンスはtarget1の約29%に縮減し、タイムも倍速くなりました。
(ちなみに、srfi-158では67%の縮減のようです。)

vs do 職人コード

do 職人のtarget3とも比較してみます。

target2と速度はあまり変わりませんが、コンスはさらに縮減できてtarget1の約5%になりました。

(equal (target1) (target3))
→ T

(time (dotimes (i *count*) (target3)))

#|| Timing the evaluation of (dotimes (i *count*) (target3))

User time = 0.213 System time = 0.000 Elapsed time = 0.203 Allocation = 38295784 bytes 0 Page faults Calls to %EVAL 160036 ||#

といっても、途中でnn^2のペア作ってないし卑怯!となってしまうと思うので、中間でペアを作りつつdo職人の結果を目指します。

seriesはストリーム的な書法の他に外部イテレータ的な書き方も可能で、下記のように書いてみました。

なぜこう書くかというと、ペアを作ってペアをばらすのを同一のスコープに収めてdynamic-extent指定し、コンスは無かったことにしたいからで、標準APIのストリーム的な書法ではちょっと難しいです。
(多分ストリームを二本作れば可能)
なおdynamic-extent指定をしなくても多値を使って同様の効果が得られます(がペアを作る縛りなので……)

計測してみると、do職人コードと遜色なくなりました。

(defun target4 ()
  (let ((g (gatherer #'collect)))
    (iterate ((x (scan-range :length size)))
      (let ((x (cons x (expt x 2))))
        (declare (dynamic-extent x))
        (when (oddp (car x))
          (let ((x (cdr x)))
            (when (zerop (mod x 3))
              (next-out g x))))))
    (result-of g)))

(equal (target1) (target4))
→ T

(time (dotimes (i *count*) (target4)))

#||| Timing the evaluation of (dotimes (i *count*) (target4))

User time = 0.232 System time = 0.000 Elapsed time = 0.220 Allocation = 38939848 bytes 0 Page faults Calls to %EVAL 160036 ||#

ちなみに、dynamic-extent指定を削除するとコンスはtarget1並みに増えるようです。

#||
Timing the evaluation of (dotimes (i *count*) (target4))

User time = 0.244 System time = 0.000 Elapsed time = 0.231 Allocation = 198940456 bytes 0 Page faults Calls to %EVAL 160036 ||#

まとめ

遅延リスト、ストリーム、ジェネレータ辺りで何かしようと思ったらseriesも結構使えることがあります。

ただseriesは色々特殊なので、もうちょっと整理された次世代seriesがあったら良いなあという近頃です。


HTML generated by 3bmd in LispWorks 7.0.0

どう書く?org: ローカル変数の一覧を取得 (Common Lisp)

Posted 2018-03-18 13:46:33 GMT

久々にonjoさんのページを眺めていて、どう書く?orgの問題を解いているページに遭遇しました。

かれこれ10年以上前のことですが、そういえばこんな問題あったなあと懐しくなりました。
下記のような問題です。

どう書く?org: ローカル変数の一覧を取得

リフレクション系のお題の続編です。 ローカル変数の内容を取得して連想配列(ハッシュ、辞書など)に詰める コードを書いてください。

Pythonで表現すると、下のコードの???部分を埋めることになります。

>>> def foo(): x = 1 y = "hello" ??? return result

>>> foo() {'y': 'hello', 'x': 1}

なお、どう書く?orgは残念ながらアダルトサイトと化してしまったようなのでリンクはarchive.orgです。

Lisp方言の他の方々の回答は、基本構文に仕込みを入れて、Pythonでいうlocalsみたいなものをサポート、という感じです。
自分は、この問題は解いたことがなかったので、色々考えてみました。

Common Lispにおいてローカル変数とは

お題はPythonが念頭にあるようですが、Lisp-1なPythonならローカルのスコープに漂っているのは変数位のものです。
しかし、Lisp-2の権化たるCommon Lispだと、関数、マクロ、シンボルマクロ、スペシャル変数等々がローカルスコープに存在しうります。

実際的にコードウォーキングして何かをする場合、ブロック、GOタグ等を含めていじること多いので、変数だけ取得できても、そんなに嬉しくない気はしますが、それはさておき解答を考えてみます。

解答その一 (実際的に考える)

実際の所、実行時にスコープ内のローカル変数名の一覧を取得したいことというのは殆どありません。
また、遅くなっても良いのだったら、別に言語のスコープのコンテクストをいじらなくても、連想配列等でエミュレートできそうです。

とりあえず、Common Lispには、実行時に名前で変数をアクセスする仕組みとしてスペシャル変数があるので、そういう時はスペシャル変数を使うのが良いだろうということで、こんな感じに書いてみました。

(defun locals () nil)

(defmacro progx ((&rest binds) &body body) `(progv ',(mapcar #'car binds) (list ,@(mapcar #'cadr binds)) (flet ((locals () (append (list ,@(mapcar (lambda (b) `(cons (quote ,(car b)) ,(cadr b))) binds)) (locals)))) ,@body)))

(defun foo (&aux result)
  (progx ((outer 42))
    (progx ((x 1)
            (y "hello"))
      (setq result (locals))
      result)))

(foo)((x . 1) (y . "hello") (outer . 42))

上記のprogxは、実行時の変数結合を実現するprogvをラップしたもので、ボディ内部のlocalsの実行で指定された変数名が取得できます。

スペシャル変数のアクセスはそこまで遅くないので、もし実際的に必要なことがあれば、こういうものでまかなえるのではないでしょうか。

その二 (定義時に処理する)

onjoさんの解答がこちらの系統ですが、onjoさんはAllegro CLのインタプリタ動作時のみ対応とのことでした。
しかし、実行時に外から飛び込んでくるレキシカル変数名というのは考慮しなくても良さそうなのと、外から飛び込んでくるスペシャル変数名は、上記progxのようなもので別途対応すれば良いだろうということで、定義時(マクロ展開時)に確定した情報を取得できれば良しという方針でシンプルに書いてみました。

#+lispworks
(defun get-local-variables (env)
  (mapcar #'compiler::venv-name (compiler::environment-venv env)))

#+sbcl (defun get-local-variables (env) (mapcar #'car (sb-c::lexenv-vars env)))

#+allegro (defun get-local-variables (env) (let* ((base (sys::augmentable-compilation-environment-base env)) #+(:version= 8) (vartab (sys::augmentable-environment-variable-hashtable base)) #+(:version>= 9) (vartab (sys::ha$h-table-ht (sys::augmentable-environment-variable-hashtable base)))) (loop :for var :being :the :hash-keys :of vartab :when (typep (sys:variable-information var env) '(member :lexical :special)) :collect var)))

(defmacro locals (&environment env) `(list ,@(mapcar (lambda (v) `(list ',v ,v)) (get-local-variables env))))

肝は、ローカル変数の一覧が取れるかどうかなのですが、Clozure CLでは取得の方法が分からず対応していません。
ローカル変数の一覧が取れる処理系であれば同様の方法で取得できるかなと思います。

試してみる

こんな感じになります。

(defun foo (&aux result)
  (let ((outer 42))
    (declare (special outer))
    (flet ((bar ()
             (let ((x 1)
                   (y "hello"))
               (setq result (locals))
               result)))
      (bar))))((y "hello") (x 1) (outer 42) (result nil))

また、変数は宣言してから使うことになるので、Pythonと違ってresultも計上されることになりますし、ローカル変数外側のスコープものも見えます。

ちなみに、Pythonで確認してみたところ、外のブロックのものは取得しない様子。

def foo():
  outer = 42
  def bar():
    x = 1
    y = "hello"
    result = locals()
    return result
  return bar()

{'y': 'hello', 'x': 1}

しかし、一番内側のブロックで変数を使えば見えるようです。
どうも、中途半端なような。

def foo():
  outer = 42
  def bar():
    x = 1
    y = "hello"
    z = outer
    result = locals()
    return result
  return bar()

{'y': 'hello', 'x': 1, 'z': 42, 'outer': 42}

まとめ

どう書くorgのページでも議論されていましたが、処理系がデバッグ情報として変数名を含める際にこういう機能が必要になることが多いと思われますが、ユーザーが触って何か有益な処理をするというのはあまり無さそうです。

なお、ご存知の通り、Common Lispでは対話的なデバッガの機能が充実しているので、ローカルな変数名等は大抵見えたり、変更したりもできます。


HTML generated by 3bmd in LispWorks 7.0.0

RMSはCommon Lispの言語仕様をとりまとめた主要人物ではない

Posted 2018-03-10 07:44:34 GMT

こんにちはCommon Lispポリスメンです。

RMSはとても偉大ですが、Common Lisp仕様の議論には参加してるものの、主要人物ではありませんでした。
ちなみにCommon Lisp仕様の議論には何十人もの人が参加しています。
また、主要人物は4人ではなく、5人でした。

  • The Evolution of Lisp

    The core group eventually became the “authors” of CLtL I: Guy Steele, Scott Fahlman, David Moon, Daniel Weinreb, and Richard Gabriel. This group shouldered the responsibility for producing the language specification document and conducting its review. The self-adopted name for the group was the “Quinquevirate” or, more informally, the “Gang of Five”.

また、ANSI Common Lisp規格策定のX3J13のメンバーでもありません。

おそらくですが、RMSとGLSが混ざってしまったのではないでしょうか(LispとEmacsあたりで)
RMSが議論に参加している様子は、仕様策定メーリングリストで眺めることができますので、ご興味があればどうぞ。

また、Common Lisp(1984)の謝辞にも登場しています。

その他おまけ

RMSが書いたっぽいLispコードの例


HTML generated by 3bmd in LispWorks 7.0.0

Common Lispのeval-whenとコンパイラかインタプリタ実装かは別のレイヤーの話

Posted 2018-03-07 20:02:22 GMT

先日Twitterでこんなやりとりを目にしました

たしかに太古のMACLISPや、Franz Lispでは、変数の結合がダイナミックだったりして、コンパイラ(静的に決まることが多かった)かインタプリタ(基本動的)かで挙動が変わることが悩みのタネだったことがありました。

しかし……Common Lispではコンパイラかインタプリタ実装かで実行に差異はない

eval-whenというからにはLispが指しているのはCommon Lispだと思いますが、Common Lispでは、評価器がコンパイラかインタプリタ実装かで違いはでないこと、と規定されています。
また、プログラムがどちらの方式で実行されているか知る手立てもありません。

evaluation n. a model whereby forms are executed , returning zero or more values.
Such execution might be implemented directly in one step by an interpreter or in two
steps by first compiling the form and then executing the compiled code; this choice is
dependent both on context and the nature of the implementation, but in any case is
not in general detectable by any program. The evaluation model is designed in such a
way that a conforming implementation might legitimately have only a compiler and
no interpreter, or vice versa. See Section 3.1.2 (The Evaluation Model).

Common Lispがするコンパイルと実装戦略は別

じゃあ、Common Lispのcompileとかcompile-fileはどうなるんだ、と思うかもしれないですが、Common Lispが規定するコンパイルプロセスは、評価器の実装戦略とは別のレイヤーの話です。

以下、ややこしいのでコンパイルプロセスの方をコンパイルと書きます。
Minimal Compilationというのが定められていますが、インタプリタのみの処理系でも、コンパイルしてロードして、というのは可能で、その場合にはコンパイルによってマクロ展開等の前処理的なものを省いたものを解釈していくことになると思われます(多分)

また逆に、evalが即ちインタプリタということもなく、フォームの一塊のツリーをコンパイルしてから処理しても問題ありません。

Minimal compilation is defined as follows:
• All compiler macro calls appearing in the source code being compiled are expanded, if at
all, at compile time; they will not be expanded at run time.
• All macro and symbol macro calls appearing in the source code being compiled are
expanded at compile time in such a way that they will not be expanded again at run
time. macrolet and symbol-macrolet are effectively replaced by forms corresponding to
their bodies in which calls to macros are replaced by their expansions.
• The first argument in a load-time-value form in source code processed by compile
is evaluated at compile time; in source code processed by compile-file , the compiler
arranges for it to be evaluated at load time. In either case, the result of the evaluation is
remembered and used later as the value of the load-time-value form at execution time.

eval-whenとは

eval-whenは、主に処理が複数に分かれてしまう(ファイルをコンパイルしてロードする等)場合に生じる問題を解消するためのもので、コンパイル中にコンパイル中に定義した関数を使いたいとか、コンパイル済みのファイルをロードする場合に特定の仕事をさせたい等々のことに利用します。

Emacs Lispのeval-when-compileも似たようなものだと思いますが、脚注のコメントによると、Common Lispの#.(リード時評価)により近いそうです。

用途としてはCommon Lispと同じではないでしょうか。インタプリタ実装でもコンパイルは可能なので、Emacs Lispは、100%インタプリタ実装のCommon Lispに近い感じなのかもしれません。

他の方々からの指摘

上記で説明したように、挙動は変わないことになっています。

上記で説明したように、eval-whenと実装戦略は別のレイヤーで、これらの組み合わせがコードの意味を変えることはありません。

まとめ

Common Lispではコンパイラかインタプリタ実装かで実行に差異はない、と書きましたが、これは過去のLispでの問題を解決する歴史の上に成り立ったものでした。
Common Lispの歴史の中でも、ANSI規格以前のCLtL1では、*applyhook**evalhook*compiler-let等、コンパイラ/インタプリタ動作で整合性が取れなくなる/取るのが難しい機能が散見されましたが、これらはANSI規格として煮詰められる際に廃止となっていたりします。

また、現代のCSの意味論からすると不思議なこともあると思いますが、CSの意味論が洗練/確立する前からLispのコンパイラ/インタプリタは存在します。
長い歴史の中で、互いに影響しあったかもしれませんし、同じような結果になったとしても道程は全然違うものかもしれません。

コンパイラ……インタプリタ……コンパイラがコンパイル……と書いてる間に段々良く分からなくなってきたので、間違いがあったらご指摘よろしくお願いします。

ちなみに何故Twitterの議論には参加していないのかというと、自分は鍵アカウントの為で、ブログで失礼しました。


HTML generated by 3bmd in LispWorks 7.0.0

マクロ展開コードの副作用から起きる問題の特別に汚い例が気になる

Posted 2018-01-21 09:01:25 GMT

On Lispの10.3 マクロ展開関数の副作用には、

MillerとBensonのLisp Style and Designは 展開コードの副作用から起きる問題の特別に汚い例に言及している. Common Lispでは...

とあります。
恐らく、「特別に汚い例に言及している.」の後は、Lisp Style and Designが言及している内容を具体的に紹介しているのだとは思いますが、いまいち文の繋がりが良くなく思え、言及しているという紹介のみで、以降は全く別の話という可能性も無くはないと思えてきてしまったので、実際にLisp Style and Designでは何が書かれていたのか確認してみました。

ネタ元

Lisp Style and Designでは、マクロの展開時の問題について解説していますが、恐らく、4.2.5.2 Problems to Avoid When Writing Macros(P.85)のようです。

この段落では、主にマクロ展開時に副作用を使うと、どういう悪い結果が齎されるかについて、具体例を挙げて解説していますが、締めに&rest&bodyのリストの破壊的変更が解説されています。

ということで、On Lisp 10.3章の「言及している」から章末までは、Lisp Style and Designでの解説を元ネタにしてます、ということだったようです。

ちなみに、On Lisp 10.3章では、副作用の例として、マクロ展開時に変数を更新することについてを取り上げていますが、Lisp Style and Designでは、変数のspecial宣言を取り上げていて、On Lispでinternの問題が追加になった以外は、両書ともほぼ同じ問題を紹介していたようです。


HTML generated by 3bmd in LispWorks 7.0.0

マクロに付くコンパイラマクロの使い道

Posted 2018-01-15 18:11:35 GMT

コンパイラマクロは関数だけでなくマクロにも定義できます。
HyperSpecのdefine-compiler-macroの項目にも明記されているのですが、一体どういう所で使うのかと思っていました。

コンパイラマクロの使い方としては、基本的にセマンティクスが変わらないことが絶対条件で、あとは何らかの効率が良くなるようなコード変換をすることになるんだと思います。
マクロでこういうパターンはどういう場合かを考えてみましたが、TAOのforみたいな場合に効率的な処理に展開できそうなので、ちょっと試してみました。

なお、マクロにコンパイラマクロを定義して、マクロなのにfuncallが効くように見せ掛けるテクニックを目にしたことがありますが、これはセマンティクスが変わってしまうのでNGかなと思っています。

TAOのfor

TAOのマニュアルによると、forは、

  形式 : for var list form1 form2  ...
form1 form2 ... を var を使って順に実行する。 var は list の各要素に
逐次束縛されたものである。 form1 form2 ... は list の長さと同じ回数評価
される。 nil を返す。

とあって殆どdolistと同じ挙動なのですが、indexというSRFI-1のiotaのような関数と組み合せて使う場合、実際には数値リストが生成されないという説明があります。

(index 0 10)(0 1 2 3 4 5 6 7 8 9 10) 

(let ((ans nil)) (for i (index 0 10) (push i ans)) ans)(10 9 8 7 6 5 4 3 2 1 0)

恐らく実際のTAOもマクロ展開のパターンマッチで展開を変えているんだと思いますが、こういうケースのマクロ展開にコンパイラマクロが使えそうです。

とりあえず、ベースの関数とマクロを定義します。

(defun index (start end &optional (step 1))
  (if (plusp step)
      (loop :for i :from start :upto end :by step :collect i)
      (loop :for i :from start :downto end :by (- step) :collect i)))

(defmacro for (var list &body body) `(dolist (,var ,list nil) ,@body))

コンパイラマクロで中間リストの生成を無くす

そしてコンパイラマクロを定義します。

下記では、無駄にメソッドを使っていますが、こういう場合には嵌るかなと思って試してみました。
メソッドディスパッチでindexだけでなく、iotaにも対応してみています。

(define-compiler-macro for (&whole whole var list &body body)
  (for-cm-expander (car list) var list body whole))

(defgeneric for-cm-expander (fn var list body whole) (:method (fn var list body whole) (declare (ignore fn var list body)) whole))

(defmethod for-cm-expander ((fn (eql 'index)) var list body whole) (declare (ignore whole)) (destructuring-bind (index start end &optional (step 1 stepp)) list (declare (ignore index)) (if stepp `(if (plusp ,step) (loop :for ,var :from ,start :upto ,end :by ,step :do (progn ,@body)) (loop :for ,var :from ,start :downto ,end :by (- ,step) :do (progn ,@body))) `(loop :for ,var :from ,start :upto ,end :do (progn ,@body)))))

(defmethod for-cm-expander ((fn (eql 'srfi-1:iota)) var list body whole) (declare (ignore whole)) (destructuring-bind (iota count &optional (start 0) (step 1)) list (declare (ignore iota)) `(loop :for ,var :from ,start :repeat ,count :by ,step :do (progn ,@body))))

こういう定義にすると、コンパイル時にはこういう展開になります。

(compiler-macroexpand '(for i (index 10 20) (print i)))
==>
(loop :for i :from 10 :upto 20 :do (progn (print i))) 

(compiler-macroexpand '(for i (srfi-1:iota 10) (print i))) ==> (loop :for i :from 0 :repeat 10 :by 1 :do (progn (print i)))

決め打ちのパターンから外れれば、リストを回す通常の処理になります。

(compiler-macroexpand '(for i (cons -1 (index 0 10)) (print i)))
==>
(for i (cons -1 (index 0 10)) (print i)) 

むすび

define-compiler-macroでマクロにコンパイラマクロを定義できるようにした経緯からユースケースを知りたかったのですが、過去のメーリングリスト等からは探し出せませんでした。

何か他のユースケースがあれば是非とも知りたいです。


HTML generated by 3bmd in LispWorks 7.0.0

LispWorks 7.1 購入への道(3) — バグ報告2件

Posted 2018-01-14 09:15:07 GMT

LispWorks 7.1を試用していて2件程バグっぽい挙動に遭遇していました。
折角試用させて貰ったので、何かバグ的なものは報告しておきたいところなので期間最終日に報告。

7.1は関数のボディで点対リストを受けつける

具体的な再現コードにすると

(defun foo (x) x . a)
→ foo

(compile nil (lambda (x) x . a)) → #<Function 15 406003A054> NIL NIL

というのが通ってしまいます。
LW 7.0では、 In a call to length of (x . a), tail a is not a LIST.

というエラーになりますが、LispWorks以外の大抵の処理系も同様のエラーとします。
7.1では、点対リストを許容するようになったのかとも思えますが、

(compile nil (lambda (x) . a))
→ Cannot take CDR of A.

こういうのは通らないので一貫性がない様子。

UTF-8のエンコーディング設定で起動時にエラーになる

もう一点、LispWorks 7.0を利用していてバグじゃないかと思いつつも適当なワークアラウンドで回避できていたのですっかり忘れていたことがありました。
具体的には、UTF-8の設定時に起動でコケるというちょっと嫌な感じのもの。

どうやらバイナリのファイル(スプラッシュ画像)をUTF-8で開こうとしてエラーになるらしいので、ファイルのエンコーディング判定関数で画像ファイルは迂回するようにしていましたが、これが7.1でも発生する様子。

;;; 回避コード
(defun utf-8-file-encoding (pathname ef-spec buffer length)
  (declare (ignore buffer length))
  (if (assoc (pathname-type pathname) graphics-ports::*file-types-to-image-types*
             :test #'equalp)
      '(:latin-1)
      (system:merge-ef-specs ef-spec '(:utf-8 :use-replacement t))))

(setq system:*file-encoding-detection-algorithm* '(utf-8-file-encoding))

報告を作成するためにちょっと追い掛けてみましたが、capi-gtk-library::put-image-data-in-fileでエラーになるらしいので、この関数のトレース結果を添付しました。

報告してみた

最終日に報告してみたところ、UTF-8の方はバグだったらしく二日後にプライベートパッチが届きました。
なんとパッチが出るとは想定していなかった。試用期間は終了しているのでパッチが試せないという……。

パッチの名前が、put-image-data-in-file.64ufaslなので、やはり報告した、capi-gtk-library::put-image-data-in-fileが問題のようです。

もったいないので、とりあえず駄目元で7.0で読み込ませてみましたが、faslに互換性がないと警告がでるものの、無視して続行可能なので試したら、7.0でも機能するようです。

ちょっと深追いして、パッチ適用前後のcapi-gtk-library::put-image-data-in-fileを比較してみると、修正前のものは、バイナリの一時ファイルを作成するのにsys:make-temp-fileというテキストの一時ファイルを作成する関数を呼んでいたため、エンコーディングの問題が発生していたようです。
修正では、sys:make-temp-fileの下請けであるsys::open-temp-fileを使ってファイルを(unsigned-byte 8)で開くようにした様子。

大抵のユーザーはlatin-1で使っていたから遭遇しなかった問題なのか、なんなのか。
とりあえず、バイナリファイルの開き方が悪かったということで、エンコーディング周りがバグってるわけではない様子なので良かったです。

ちなみに7.0で強制的に読み込ませるのには、

(handler-bind ((fasl-error #'continue))
  (scm:require-private-patch "put-image-data-in-file" :capi-gtk))

としています。

7.0用のパッチが貰えるなら、貰った方が良いのかもしれない。

試しにdisassembleの内容からコードを推測しつつ自作しててみましたが、下記と等価なようです。
(コンパイルオプションを設定すればdisassembleの結果が同じになる)

(in-package :cg-lib)

(declaim (optimize (safety 3) (speed 1) (float 1) (sys:interruptible 0) (compilation-speed 1) (debug 2) (hcl:fixnum-safety 3)))

(defun put-image-data-in-file (image-data) (destructuring-bind (file-type . data) image-data (let ((out (sys::open-temp-file :file-type file-type :element-type '(unsigned-byte 8) :external-format :latin-1))) (write-sequence data out) (close out) (namestring out))))

このコードを

(let ((source-debugging-on-p (source-debugging-on-p)))
  (toggle-source-debugging nil)
  (compile-file "put-image-data-in-file.lisp" :debug-info nil)
  (toggle-source-debugging source-debugging-on-p))

のような感じでコンパイルすれば、大体提供されているパッチファイルと同じになるようです。

LispWorks 7.0のパッチが入手できない場合は、最悪これでも良いかもしれません。

もう一つのボディが点対リストを許容する件については、一週間後位に回答があり、次期バージョンで善処したいということでした。

むすび

試用期間のバグ報告は、パッチが期間内に試せるように早めに報告しよう。


HTML generated by 3bmd in LispWorks 7.0.0

LispWorks 7.1 購入への道(2) — Common Prologを試す

Posted 2018-01-10 18:01:30 GMT

LispWorksでは、Enterprise版でしか使えない機能はいくつかありますが、中でも自分はKnowledgeWorksに興味があったので、まずはこれを試してみたいところ。

KnowledgeWorksは、Common Lispで構成された古き良き知識ベースのシステムで、主に後ろ向き推論のPrologと、前向き推論のOPS5が合体したようなシステムです。
1980年代後半から90年代前半の商用のLispシステムでは、エキスパートシステム作成用にOPS5やProlog拡張が付属することが多かったようで、こういう構成は案外定番の構成だった様子。
(ちなみに、Prologの処理系でも前向き推論の拡張等は良く付属していたようです。)

Prolog部分は、Common Prologと呼ばれていて、これ単体でも使えます。
ということで、このブログでは毎度お馴染のZebraベンチを書いてみました。

Common PrologでZebra ベンチ

;;;; -*- Mode: common-lisp; Syntax: Common-Lisp -*-
(declaim (optimize (speed 3) (safety 0) (compilation-speed 0)
                   (debug 0)))

(eval-when (:compile-toplevel :load-toplevel :execute) (require "prolog"))

(defpackage :clog-zebra (:use :cl :clog))

(in-package :clog-zebra)

(defrel nextto ((nextto ?x ?y ?list) (iright ?x ?y ?list)) ((nextto ?x ?y ?list) (iright ?y ?x ?list)))

(defrel iright ((iright ?left ?right (?left ?right . ?rest))) ((iright ?left ?right (?x . ?rest)) (iright ?left ?right ?rest)))

(defrel zebra ((zebra ?h ?w ?z) ;; Each house is of the form: ;; (house nationality pet cigarette drink house-color) (= ?h ((house norwegian ? ? ? ?) ;1,10 ? (house ? ? ? milk ?) ? ?)) ; 9 (member (house englishman ? ? ? red) ?h) ; 2 (member (house spaniard dog ? ? ?) ?h) ; 3 (member (house ? ? ? coffee green) ?h) ; 4 (member (house ukrainian ? ? tea ?) ?h) ; 5 (iright (house ? ? ? ? ivory) ; 6 (house ? ? ? ? green) ?h) (member (house ? snails winston ? ?) ?h) ; 7 (member (house ? ? kools ? yellow) ?h) ; 8 (nextto (house ? ? chesterfield ? ?) ;11 (house ? fox ? ? ?) ?h) (nextto (house ? ? kools ? ?) ;12 (house ? horse ? ? ?) ?h) (member (house ? ? luckystrike oj ?) ?h) ;13 (member (house japanese ? parliaments ? ?) ?h) ;14 (nextto (house norwegian ? ? ? ?) ;15 (house ? ? ? ? blue) ?h) (member (house ?w ? ? water ?) ?h) ;Q1 (member (house ?z zebra ? ? ?) ?h) ;Q2 ))

;; (logic '(zebra ?h ?w ?z) :return-type :fill) (defun zebra-benchmark (&optional (n 1000)) (declare (optimize (speed 3) (safety 0))) (let (rt0 rt1) (time (loop initially (setf rt0 (get-internal-run-time)) repeat n do (logic '(zebra ?h ?w ?z) :return-type :fill) finally (setf rt1 (get-internal-run-time)))) (destructuring-bind (houses water-drinker zebra-owner) (logic '(zebra ?houses ?water-drinker ?zebra-owner) :return-type :bag :bag-exp '(?houses ?water-drinker ?zebra-owner)) (values (/ (* n 12825) (/ (- rt1 rt0) 1000.0)) ; real time ; is milliseconds zebra-owner water-drinker houses))))

CL-USER 1 > (clog-zebra::zebra-benchmark)
Timing the evaluation of (LOOP CLOG-ZEBRA::INITIALLY (SETF CLOG-ZEBRA::RT0 (GET-INTERNAL-RUN-TIME)) COMMON-PROLOG:REPEAT CLOG-ZEBRA::N DO (COMMON-PROLOG:LOGIC (QUOTE (CLOG-ZEBRA::ZEBRA CLOG-ZEBRA::?H CLOG-ZEBRA::?W CLOG-ZEBRA::?Z)) :RETURN-TYPE :FILL) CLOG-ZEBRA::FINALLY (SETF CLOG-ZEBRA::RT1 (GET-INTERNAL-RUN-TIME)))

User time = 2.888 System time = 0.000 Elapsed time = 2.868 Allocation = 447980280 bytes 0 Page faults 4440789.5 CLOG-ZEBRA::JAPANESE CLOG-ZEBRA::NORWEGIAN ((CLOG-ZEBRA::HOUSE CLOG-ZEBRA::NORWEGIAN CLOG-ZEBRA::FOX CLOG-ZEBRA::KOOLS CLOG-ZEBRA::WATER CLOG-ZEBRA::YELLOW) (CLOG-ZEBRA::HOUSE CLOG-ZEBRA::UKRAINIAN CLOG-ZEBRA::HORSE CLOG-ZEBRA::CHESTERFIELD CLOG-ZEBRA::TEA CLOG-ZEBRA::BLUE) (CLOG-ZEBRA::HOUSE CLOG-ZEBRA::ENGLISHMAN CLOG-ZEBRA::SNAILS CLOG-ZEBRA::WINSTON CLOG-ZEBRA::MILK CLOG-ZEBRA::RED) (CLOG-ZEBRA::HOUSE CLOG-ZEBRA::SPANIARD CLOG-ZEBRA::DOG CLOG-ZEBRA::LUCKYSTRIKE CLOG-ZEBRA::OJ CLOG-ZEBRA::IVORY) (CLOG-ZEBRA::HOUSE CLOG-ZEBRA::JAPANESE CLOG-ZEBRA::ZEBRA CLOG-ZEBRA::PARLIAMENTS CLOG-ZEBRA::COFFEE CLOG-ZEBRA::GREEN))

以前、Common Lispの埋め込みPrologを試してみる: Zebraベンチ篇で、ベンチ対決した結果と並べるとこんな感じです。

順位 処理系 タイム(秒)
1 AZ-Prolog 0.710 1
2 Allegro Prolog 0.852 1.2
3 SWI-Prolog 1.422 2
4 Common Prolog 2.888 4
5 PAIProlog 11.712 16.5
6 Uranus 51.080 71.9

むすび

PAIPrologの約6倍速く、Allegro Prologの3.4倍遅く、最速のAZ-Prologからは約4倍遅いという結果になりました。
Allegro Prologが謎の速さをみせていますが、Common Prolog(KnowledeWorks)は多機能で、開発環境もリッチですし、全体的なバランスとしては、なかなか良いのではと感じています。

関連エントリー


HTML generated by 3bmd in LispWorks 7.0.0

LispWorks 7.1 購入への道(1)

Posted 2018-01-08 16:48:54 GMT

昨年末の11月13日、LispWorks 7.1が発表されました
LispWorks 7.0が2015-05-05の発表だったので、約二年半ぶりのバージョンアップです。

LispWorks 7.0を使い始めて二年ちょっと経過しましたが、毎日使っているとはいえ、あまり使い倒した感もないため、このまま7.0で行くか、とりあえず、7.1に上げるか悩ましい所ですが、一ヶ月試用できるサービスがあるので、出てすぐの11月17日に試用を申し込んでみることにしました。

お察しの通り、もうとっくに試用期間も終わってしまっているのですが、7.1について何一つブログに書いていなかったので何回かに分けて書いてみます。

試用の申し込み

試用については、二年前のエントリーと全く同じ手順でしたが、既存ユーザーだからか毎度ありがとうというような返事が来ました。

前回は、HobbyistDVでの試用でしたが、今回は、折角なので、HobbiestとEnterpriseの二種で申し込んでみました。

格が違うとはいえ、ライセンスキーが違うのみで、配布されているファイルは全バージョン共通のようです。

ライセンスキーが届いたので、ファイルをダウンロードし、早速、以前報告したバグがどうなっているか確認してみました。

7.0でのバグは修正されたのか

さて、7.0を使っていて、何度かバグ報告をしてパッチを貰ったりはしていましたが、修正はされているのかが気になりますので、早速確認。

一応、リリースノートには目を通しましたが、そんなに細かいものまで書いてはいないようです。

シンボルのバグ: 修正済み

7.0では、UTF-8環境で、(eq '資料コード '資料コード) → nilだったりしたので、報告してプライベートパッチを貰ったりしていましたが、治っていました。これは良かった。
しかし、これ7.0で公開パッチ出して欲しかった所ですなあ。

libssl 1.1の読み込みに失敗する: 対応済み

libssl 1.1を読み込ませる場合に明示に指定しないといけない件で問い合わせましたが、次バージョンで対応とのことでした。
リリースノートにもある通り対応されていたので良かったです。

システムの内部関数で型宣言が合ってないものがある: 据置き

sys::find-external-symbolの型宣言が合ってないのでコンパイルするとエラーになる件でしたが、内部関数は使うな、という回答を貰っていました。
確認した所こちらは据置き。
プロダクション用コンパイルのオプションだと無視しちゃうから気にしてない、とかそういう感じだとは思うけど、いやでも変だと思うんだよなあ。

その2に続く

関連エントリー


HTML generated by 3bmd in LispWorks 7.0.0

逆引きCommon Lisp/Scheme・Common Lisp Users JPサイトを移動しました

Posted 2018-01-05 21:59:04 GMT

立ち上げて早十年の逆引きCommon Lisp/Schemeサイトですが、Shibuya.lisp時代には長期間サイトが落ちていてもなかなか復旧されないため個人のサーバーに移動してみたり転々としています。
先日、再び新しいサーバーに移動することになったので、このタイミングでLispの情報集積サイトである lisphub.jpに移動することにしました。
さらについでで、2010年に立ち上げた Common Lisp Users JP も移動し、若干URLに統一感を持たせる感じで一緒のサイトに設置しました。

旧URLからは301でリダイレクトされるのでリンクから辿ってくるぶんには使い勝手に変化はありません。

ちなみに、 lisphub.jp ですが、最低10年は保たれるLispのハブサイトを目指して、2013年に立ち上げましたが、広報活動で色々失敗しており、2023年までドメインだけは確保しているという悲しい状況なので今後は活用していきたい所存です……。


HTML generated by 3bmd in LispWorks 7.0.0

2017年振り返り

Posted 2018-01-03 10:24:45 GMT

毎年振り返りのまとめを書いていたのですが、ホストしているサーバーの移行をしていて、2017年中に間に合いませんでした。
このブログを配信しているteepeedee2が上手く動かせなかったというのが主な理由ですが、teepeedee2は毎度ながら手強い……。

Lisp的進捗

ブログ

ですます調をやめて、である調にしましたが、どうもしっくり来ないので、今年からはまた、ですます調に戻します……。

である調でも書けるようになってみたかったのですが、どうも過剰に偉そうな雰囲気になってしまうという。

学習

毎年、Advent Calendarで少しだけ無理をして何かを調べて書いていましたが、2017は気力がないのでやめてしまいました。
しかし、やっぱり何かやっておけば良かったなあという思いが残る……。
2018年はがんばりましょう。

LispWorks

LispWorks 7.1が2017年11月に登場し、自分も試用を申し込んで、それなりに7.1の知見は貯まりましたが、全然記事にしていません。
7.0からHobbyist Edition($750)が登場し以前よりは、身近になった気はしますが……。

去年読んでる途中と報告した、LispWorks User Guide and Reference Manualとか、CAPI User Guide and Reference Manualは未だに読み終えていません。

仕様とかマニュアルを読み通すって結構大変なんですよねえ……。 なんかしらちょっとは書きたいと思います。

来年やってみたいこと

昨年の目標であった、

  • 積極的にLisp本の積読本を読んで記事にして行きたい
  • LispWorksのマニュアル全部を通読したい
  • VMS/VAXのエミュレータでVAX LISPを動かしたい
  • Lisp組み込み系Prologを系統立てて比較してみたい
  • Shenをもうちょっと触りたい
  • ヒューイット先生について調べる

は、VAX LISPがライセンスの関係で動かなかったことがはっきりした以外は何も進捗がないですね。
いやあ、時間がない訳では全くないのですが……。

やりたいことは大して変化なしなので、継続としたいと思います。
今年は、もうちょっと活動的になりたいですね。

過去のまとめ


HTML generated by 3bmd in LispWorks 7.0.0

マクロ禁止令

Posted 2017-12-13 11:52:38 GMT

Lisp Advent Calendar 2017 十三目です。
空きがあったのでネタで埋めようと思い書きました。
十三目書いてたのに!という方は、すいませんが、まだ空きがあるので他の日を埋めてください。

マクロが禁止されたらどうなるの

良くも悪くも誤解が多いLispマクロ。
Lispマクロに心酔するあまり過大評価する人もいれば、過大評価する人をみて過小評価に転ずる人もいる始末ですが、基本的にはコードを生成するだけの機能です。

そんなマクロですが、Common Lispで仮に禁止されたらどうやって生きていったら良いのか考えてみました。
(ちなみに話を簡単にするためにローカルマクロのことは考えないことにします。)

defmacroは何をしているの

Common Lispのdefmacroで定義するものは、リストを引数にして、リストを返すという関数です。
しかし、評価の前に再帰的にマクロを展開するフェイズがあり、そこで展開関数が実行されるので、まるで関数評価のような感じで使うことができます。

例えば、下記のようなコードでloopの展開関数だけ実行することも可能です。

(funcall (macro-function 'loop)
         '(loop :for i :from 0 :repeat 10 :collect i)
         nil)

関数だけでdefmacroのようなことをしてみる

例としてdotimesのようなものを考えてみましょう

'(dotimes (i 10) (princ i))

のようなリストを

'(prog ((#:|limit17589| 10) (i 0))
      "=>"
      (cond ((<= #:|limit17589| i) (return (let ((i nil)) nil))))
      (progn (princ i))
      (incf i)
      (go "=>"))

のようなリストに変形すれば良いので、

(defun mydotimes (form &optional env)
  (declare (ignore env))
  (destructuring-bind (_ (var limit &optional result) &body body)
                      form
    (declare (ignore _))
    (let ((limvar (gensym "limit"))
          (tag (gensym "=>")))
      `(prog ((,limvar ,limit)
              (,var 0))
        ,tag (cond ((<= ,limvar ,var)
                    (return (let ((,var nil)) ,result))))
             (progn ,@body)
             (incf ,var)
             (go ,tag)))))

のような関数を書けるでしょう。
(まあ、結局の所マクロを書く作法が身に付いていないと、こういう関数も書けないのですがそれは一旦忘れましょう)

これで、下記のように書けます。

(with-output-to-string (out)
  (declare (special out))
  (eval (mydotimes 
         `(mydotimes (i 3)
            ,(mydotimes '(mydotimes (j 3) 
                           (princ i out)
                           (princ j out)))))))
→ "000102101112202122" 

やはりマクロ展開と実行コードを混ぜて書かないといけないので、ごちゃごちゃしてしまいます。
別個にマクロ展開関数を用意して、オペレーターが定義したマクロかどうかを確認しつつ展開するようにすれば、

(with-output-to-string (out)
  (declare (special out))
  (mexpand `(mydotimes (i 3)
              (mydotimes (j 3) 
                (princ i out)
                (princ j out)))))
→  "000102101112202122" 

位には圧縮できるかもしれません。

もうちょっと綺麗にできないか

とりあえずは、安直に簡単に見た目を変える方向で、リーダーマクロを使ってごちゃごちゃを隠してみましょう。
見た目がごちゃごちゃしているだけではなく、上記では、変数の結合も実行時にしているので、変数をダイナミック変数に指定していたりします。この辺りもリーダーマクロで読み取り時に展開してしまえば解決です。

なお、リーダーマクロも禁止ならファイルを2パスで処理する等々しかないですね。

(set-syntax-from-char #\] #\))
(set-macro-character 
 #\[ 
 (lambda (s c)
   (declare (ignore c))
   (let ((form (read-delimited-list #\] s T)))
     (funcall (car form) form))))

(with-output-to-string (out)
  [mydotimes (i 3)
    [mydotimes (j 3)
      (princ i out)
      (princ j out)]])
→ "000102101112202122" 

結論

結局の所、

  • コードがデータ
  • 評価器に渡るコードを変形するフックがユーザーに開放されている

の2点が言語に備わっていれば、Lispマクロのような機能と使い勝手は実現可能だということが分かるでしょうか。
特にLispには限らない筈ですが、使い勝手を含めて真面目に活用が考えられてきた、また実績があるのは、ほぼLisp系言語のみ、というのが現状だと思います。

誕生当初は、LispもM式→S式の変換をして実行するものと考えられていたLispですが、S式というデータの世界にLispプログラマが飛び込んだことが偉大だったのかもしれません。


HTML generated by 3bmd in LispWorks 7.1.0

世界から括弧が消えたなら

Posted 2017-12-07 17:20:25 GMT

Lisp Advent Calendar 2017 八日目です。
空きがあったのでネタで埋めようと思い書きました。
八日目書いてたのに!という方は、すいませんが、まだ空きがあるので他の日を埋めてください。

括弧が見えなくなったらどうなるの

Emacs等のエディタでは括弧だけ薄い色にしたりできるようですが、Unicodeの幅がない文字で置き換えたらどうでしょうか。

UTF-8のCommon Lispならこんな感じの設定にすればOKでしょう

(progn
  (set-macro-character
   (code-char #x200C)
   (lambda (s c)
     (declare (ignore c))
     (read-delimited-list (code-char #x200D) s T)))
  (set-syntax-from-char (code-char #x200D) #\)))

そうしたら、こう書けます。

‌defun fib ‌n‍
  ‌if ‌< n 2‍
     n
     ‌+ ‌fib ‌1- n‍‍
       ‌fib ‌- n 2‍‍‍‍‍

‌fib 10‍ → 55

いやあ、読み難いなあ。

エディタに支援してもらおう

Emacsには文字に構文上の意味が持たせられるので、設定しておくと便利かもしれません。

(progn
  (modify-syntax-entry 8204 (format "%c%c" 40 8205))
  (modify-syntax-entry 8205 (format "%c%c" 41 8204)))

これで若干編集が効きますが、どうもあまり上手くいかない。

見えない括弧の応用例

Clojureっぽく書きたい人へ

Clojureは括弧が少ないんだ優勝だという人は、こういう感じはどうでしょうか。

(defun fib (n)
  (cond(< n 2) n‍
        ‌:else (let (‌n1 (1- n)‍
                    ‌n2 (- n 2))
                (+ (fib n1)
                   (fib n2)))))

まあまあですね。

オリジナルのポーランド記法にこだわる

Lispの表記法は、括弧を明示するので、Cambridge Polish Notationなどとも呼ばれます。
しかし、オリジナルのポーランド記法は、アリティが決まっているなら括弧を不要にできることこそがその特長だったようです。
その意向を汲んでみました。

(defun fib (n)if (< n 2)
     n
     (+ ‌fib ‌1- n‍‍
        ‌fib (- n 2)))

アリティを暗記していないといけないので、読み書きで脳に負担がかかりそうですね。

結論

括弧は脳に優しい。


HTML generated by 3bmd in LispWorks 7.1.0

Common Lisp と タイムゾーン と Zetalisp

Posted 2017-12-04 15:00:01 GMT

Lisp Advent Calendar 2017 五日目です。

Common Lisp は移動体上でのタイムゾーンを意識して設計されている?

Common Lispのタイムゾーンについては移動体のことを考慮し、定数になっていないというような話が前々から気になっていたので、実際のところどうなのだろうと思って調べてみた。

元ネタ

多分、元ネタはCLtL2なんだろうと思うので検索してみると、CLtL2のget-decoded-timedecode-universal-timeの注釈にある、

Compatibility note: In Lisp Machine Lisp time-zone is not currently
returned. Consider, however, the use of Common Lisp in some mobile
vehicle. It is entirely plausible that the time zone might change from
time to time.

だと思われる。

この注釈の解釈だけれど、Common Lispがタイムゾーンを意識してどうのこうのしたというよりは、互換性にかこつけて、Zetalispがタイムゾーンを返さないことについて細かいツッコミをいれているように思えるのだがどうだろうか。

CLtL1も確認してみたら、同様の記述だったので、1984年時点の認識らしい。

移動体であればCommon Lisp処理系は結局ホストのOSかどこかからタイムゾーンの情報を貰ってくることになるが、基盤となるuniversal-timeは1900-01-01T00:00Zからの秒数なので基本的にこれが動くこともなく、どこのタイムゾーンとしてデコードするか、という話になる。
ちなみに、今どのタイムゾーンにいるかを処理系がホストと通信したりして把握する手順/機構のようなものはCommon Lispの規格上には定義されていない。

文句を付けられていたZetalisp(Lisp Machine Lisp)は実際どうだったのか

それで、実際Zetalisp(Lisp Machine Lisp)がどうだったのかをソースで確認してみたが、1982年頃のSystem 78.48では確かにタイムゾーンは返していなかった。
しかし、1984年のSystem 99のソースとその時期のマニュアルであるChinual 6ではタイムゾーンを返すようになっているので、CLtL1が出版された前後で既にCLtL1のZetalispとの互換性の注釈は、時代遅れなものになっていたらしい。

1984年は、MIT LispMもCommon Lisp化した時期なので、ZetalispがCommon Lispを取り込んでしまった、もしくは、ZetalispがCommon Lisp化した、とも考えられるので微妙ではあるが……。
1983年のChinual 5を眺めるとまだタイムゾーンは返していないようなのでCLtL1執筆時のツッコミがChinual 6に影響したのかもしれない。

しかし、1990年に出版されたCLtL2でも、この趣味的な記述がアップデートされることもなく、Zetalispはタイムゾーンを返さないといわれっぱなしで今に至ることになったらしい。

ちなみに、Zetalispでもuniversal-timeへのエンコードについては1980年より前からタイムゾーンは指定する仕様になっているので、デコードされたuniversal-timeから元のuniversal-timeが復元できない非対称性についてのツッコミであったかもしれない。

まとめ

Lisp関係の伝説においては、誰も気にしないので検証もされないような趣味的なものが延々と語り継がれることって多い気がする。
折角なのでスマホ上の処理系でGPSからデータを取得してuniversal-timeをデコードするようなものがあったら面白いかもしれない。


HTML generated by 3bmd in LispWorks 7.1.0

Hexstream CLOS MOP Specの紹介

Posted 2017-11-30 17:40:58 GMT

今年も始まりました、Lisp Advent Calendar 2017 第一日目です。
今回は、Hexstream CLOS MOP Specを紹介します。

Hexstream CLOS MOP Specというのは私が勝手に命名したものなので正式名称でもなんでもないのですが、 HexstreamことJean-Philippe Paradis氏がまとめたCLOS MOPのオンラインリファレンスです。

正式名称は、Common Lisp Object System Metaobject Protocol と素材そのままのようですね。

CLOS MOPとはなんぞや

そもそもCLOS MOPとは、ということになるのですが、Common Lispのオブジェクトシステムは、オブジェクト指向プログラミングによって、ユーザーがカスタマイズ可能です。
インスタンスの生成、スロット(メンバ変数)アクセス、継承の仕方等々が、プロトコルとしてまとめられているのですが、操作対象は通常のオブジェクトから一段階メタになってメタオブジェクトとなります。
通常のオブジェクトが車だとしたら、車を組み立てるロボット(仕組み)が更にまたオブジェクトになっていて色々いじれるという感じです。
車を組み立てるロボットをカスタマイズすることにより、車の作り方や、作られる車の構成をカスタマイズ可能、くらいの所でしょうか。

CLOS MOPでのメタオブジェトは総称関数、メソッド、スロット定義、メソッドコンビネーション、スペシャライザ等がありますが、Hexstream CLOS MOP Spec ではすっきり図になっているので、どんな感じの構成になっているかわかりやすいと思います。

CLOS MOPの仕様について

残念ながらこのCLOS MOPですが、ANSI Common Lispの規格には組込まれることはありませんでした。
割と早い段階からCLOS三部構成のうち、三番目に位置するものとされ、仕様も詰められていましたが、1990年代初頭当時としては野心的過ぎたのか、うちの処理系ではMOPはサポートしないと明言するような主力ベンダーも現われたり、紆余曲折あって、最初の二部までがANSI Common Lispとして規格化される、という流れになりました。

規格としてはまとまらなかったものの、その前の段階の準備や成果が、The Art of the Metaobject Protocolという書籍としてまとめられます。
(正確には同時進行ですが)
通称AMOPとして有名な本ですが、この本の5章と6章はCLOS MOPの仕様がまとめられた章で、この仕様の部分はオンラインでも公開され、CLOS MOPの仕様といえば、この公開されたAMOPの仕様部分ということになっています。

このAMOPのCLOS MOP仕様部分は、TeX等の形式で配布されていましたが、1997年に当時Eclipse Common Lispを作っていた Elwood Corporation の Howard R. Stearns氏がHyperSpecに似た感じのhtml形式にして公開しました。

これが広く長らく使われていて、Franzなどもマニュアルの一部として配布しています。

しかし、Elwood版は、それほど使い勝手が良いとはいえず、改良するにもライセンスとして改変不可だったりするので、元のTeXからhtmlを仕立てる人が出てきたという所で、Robert Strandh氏や、今回紹介するHexstream氏のバージョンがそれにあたります。

Strandh氏のものはシンプルにまとめてあり、さらに注釈も添えられ、HyperSpecへのリンクもあるので、読み進めるのに便利です。
このStrandh氏のまとめたものを更に体裁よくまとめたものが、Hexstream氏のバージョン、というところです。

たとえば、Hexstream氏のものはプロトコルごとに眺められたりして、なかなか良いです。

また、関数名がidになっているので、Emacs等からドキュメントを検索するのも楽です。
簡単な関数を書けば、ブラウザで開くことも可能でしょう。

(defun amop-lookup (&optional symbol-name)
  (interactive)
  (let ((name (or symbol-name
                  (thing-at-point 'symbol))))
    (setq name (subseq name (1+ (or (string-match ":" name) -1))))
    (browse-url
     (format "https://clos-mop.hexstreamsoft.com/generic-functions-and-methods/#%s"
             name ))))

(c2mop:effective-slot-definition-class ...)

のようなシンボルの上で、M-x amop-lookupすれば、該当のページが開きます。

まとめ

今回は、Hexstream CLOS MOP Specを紹介しました。

とっつきにくいAMOPのリファレンスですが、綺麗にまとまっていると読み進めるのが楽で良いですね。


HTML generated by 3bmd in LispWorks 7.1.0

ボディが無限リスト 其の二

Posted 2017-11-15 12:02:15 GMT

大分古くからあるものだけれど、Olin Shivers氏が式のボディ部が無限リストになっているという面白いアイデアを書いていて、以前ちょっと考えてみたりした。

無限リストに関数を詰めて呼んでみたり、という感じだったけれども、より直接的には、evalが使えるなと考えた。

なお、評価はちょっと間違うと無限ループになるので注意。

(progv '(i) '(0)
  (catch :exit
    (eval
     '(progn . #0=((when (= 1000 i) (throw :exit :done))
                   (print i)
                   (incf i)
                   . #0#)))))

以上、暇だったので。


HTML generated by 3bmd in LispWorks 7.0.0

続ClaspがSBCLより速くなったと聞いて

Posted 2017-11-03 19:13:46 GMT

昨日のエントリーで「なぜかbench-stringというベンチでClaspがズバ抜けて速い」と書いたが、Twitterでこの件について反応があった。

BENCH-STRING が速いの、最適化でその部分のコードがごそっと削除されてるからとかいうオチがありそうな。
Kei @tk_riple

なるほど、最適化によるコード削除疑惑。
Common Lispの場合、最適化した際にdotimes等で返り値を使わない場合によく消えたりはするかもしれない。
しかし、消えてる感じにしては遅いので、まあまあそんなものかなと思っていた。
また、現状のClaspはまだ最適化がどうの、というより、まずは正しく動かすフェイズな気がするので、そんなに最適化もがんばってはいないという印象を持っていた。
実際、最適化の指定をしても型のヒントを与えても全然効いてないように思う。

結論: Claspのバグが原因で実行されないコードがあったため速かった

結論から書いてしまうと、Claspのfillのバグが原因でfill以降が実行されない為に速かった。
なので、最適化ではないけれど、searchの部分のコードが削除されていた状態になっていた。
一応順を追って説明してみる。

まず、元のコードについて。

(defun bench-strings (&optional (size 1000000) (runs 50))
  (declare (fixnum size))
  (let ((zzz (make-string size :initial-element #\z))
        (xxx (make-string size)))
    (dotimes (runs runs)
      (and (fill xxx #\x)
           (replace xxx zzz)
           (search "xxxd" xxx)
           (nstring-upcase xxx))))
  (values))

同じ長さの大きい文字列を2つ作ってfillで‘x’で埋め、もう片方でreplaceし、“zzzz…”という文字列にしてしまう。
それをsearchで“xxxd”について検索するが見付からないので、nstring-upcaseは実行されない、という流れ。

andで繋いでいるのは、指摘があったような最適化でコードが消えるのを防いでいるのかもしれない。

とりあえず、dotimesの中身を全部消したものと比較すると、全部消したものがずっと速いので、中身を全部消しているということはなさそう。

そこで一つずつ足していってみたが、そこでClaspのvectorに対してのfillの返り値がnilであることに気が付いた。
仕様では、fillsequenceを返すことになっているが、nilが返ってしまうとandで繋いでいるだけに以降が実行されないことになってしまう。

Claspのソースを確認してみると、

(defun fill (sequence item &key (start 0) end)
  ;; INV: WITH-START-END checks the sequence type and size.
  (reckless
   (with-start-end (start end sequence)
     (if (listp sequence)
         (do* ((x (nthcdr start sequence) (cdr x))
               (i (- end start) (1- i)))
              ((zerop i))
           (declare (fixnum i) (cons x))
           (setf (first x) item))
         (si::fill-array-with-elt sequence item start end)))))

となっていて、vectorの場合は、si::fill-array-with-eltの返り値となるが、そのsi::fill-array-with-eltはClaspらしくC++で書かれていた。

/*! Fill the range of elements of the array,
   if end is nil then fill to the end of the array*/
CL_LISPIFY_NAME("core:fill-array-with-elt");
CL_DEFUN void core__fillArrayWithElt(Array_sp array, T_sp element, cl_index start, T_sp end) {
    size_t_pair p = sequenceStartEnd(core::_sym_fillArrayWithElt,
                                     array->arrayTotalSize(),start,end);
    array->unsafe_fillArrayWithElt(element,p.start,p.end);
  }

core__fillArrayWithEltvoidなので、多分CLの世界ではnilを返すことになるのだろう。

ということで、

(defun fill (sequence item &key (start 0) end)
  ;; INV: WITH-START-END checks the sequence type and size.
  (reckless
   (with-start-end (start end sequence)
     (if (listp sequence)
         (do* ((x (nthcdr start sequence) (cdr x))
               (i (- end start) (1- i)))
              ((zerop i))
           (declare (fixnum i) (cons x))
           (setf (first x) item))
         (si::fill-array-with-elt sequence item start end))
     sequence)))

(fill (make-string 42) #\*) → "******************************************"

のように修正し、再度SBCLと比べてみた

バグを修正して再計測: 最適化指示ありなしで比べてみる

最適化指示なしだと依然としてClaspの方が3倍位速いらしい。
しかし、指示ありだと、SBCLがClaspの2倍位速くなった。
一応SBCLの方のdisassemble結果を確認したが、中身がごっそり消されているということはなかった。

Claspの方は、最適化指示ありでもなしでもあまり変わらず。
従来のCL処理系は、普段は遅めだけど、追い込むと速い、という傾向があるが、Claspは、普段から速めで、追い込んでもそんなに速くならない系になっていくのかもしれない。

なお、一応Claspにバグ報告は出してみた。

SBCL

(bench-strings)
;=> nil
#|------------------------------------------------------------|
Evaluation took:
  0.595 seconds of real time
  0.594982 seconds of total run time (0.594982 user, 0.000000 system)
  100.00% CPU
  1,958,880,795 processor cycles
  8,000,064 bytes consed

Intel(R) Xeon(R) CPU E3-1230 v3 @ 3.30GHz |------------------------------------------------------------|#

Clasp

(bench-strings)
;=> nil
#|------------------------------------------------------------|
Real time           : 0.208 secs
Run time            : 0.208 secs
Bytes consed        : 8026792 bytes
LLVM time           : 0.000 secs
LLVM compiles       : 0
clang link time     : 0.000 secs
clang links         : 0
Interpreted closures: 0
nil
 |------------------------------------------------------------|#

SBCL 最適化指示あり

(defun bench-strings+ (&optional (size 1000000) (runs 50))
  (declare (optimize (speed 3) (safety 0) (debug 0)))
  (declare (fixnum size runs))
  (let ((zzz (make-string size :initial-element #\z))
        (xxx (make-string size)))
    (declare (simple-string xxx zzz))
    (dotimes (runs runs)
      (and (fill xxx #\x)
           (replace xxx zzz)
           (search "xxxd" xxx)
           (nstring-upcase xxx))))
  (values))

(bench-strings+) ;=> nil #|------------------------------------------------------------| Evaluation took: 0.104 seconds of real time 0.103990 seconds of total run time (0.103990 user, 0.000000 system) 100.00% CPU 342,637,002 processor cycles 8,000,064 bytes consed

Intel(R) Xeon(R) CPU E3-1230 v3 @ 3.30GHz |------------------------------------------------------------|#

Clasp 最適化指示あり

(proclaim '(optimize (speed 3) (safety 0) (debug 0)))

(defun bench-strings+ (&optional (size 1000000) (runs 50)) (declare (fixnum size runs)) (let ((zzz (make-string size :initial-element #\z)) (xxx (make-string size))) (declare (simple-string xxx zzz)) (dotimes (runs runs) (and (fill xxx #\x) (replace xxx zzz) (search "xxxd" xxx) (nstring-upcase xxx)))) (values))

(bench-strings+) ;=> nil #|------------------------------------------------------------| Real time : 0.222 secs Run time : 0.222 secs Bytes consed : 8026792 bytes LLVM time : 0.000 secs LLVM compiles : 0 clang link time : 0.000 secs clang links : 0 Interpreted closures: 0 nil |------------------------------------------------------------|#


HTML generated by 3bmd in LispWorks 7.0.0

ClaspがSBCLより速くなったと聞いて

Posted 2017-11-02 19:05:04 GMT

Shibuya.lisp Lispmeetup #57 の発表でClaspがSBCLより速かったりするらしいというのを耳にして、いつの間にかそこまで進歩してたのかと思ったので早速自分も試してみることにした。

ビルドは、本家のWikiの通りに実行し特に問題もなくビルドはできた。ただ時間は、3時間程度掛った。
Clasp 0.5 Build Instructions

cl-benchで計測してみる

3年位前にclaspが登場した頃に、一度clasp 0.2でcl-benchを実行してみたが、完走できない項目ばかりだった。
今回のclasp 0.5で試してみたところ大体の項目が完走できた。しかし物によってはSegmentation faultで処理系ごと落ちたりもする。
cl-bench は、Symbolics CLや、Lucid CLでも走る位なので可搬性は高い、というか規格内の機能だけで書いてある(多分)。

cl-benchや手元で確認してみる感じでは、SBCLより速かったりすることは無さそうに思えた。
さらに安定性については、まだ比較対象にならないという感じ。

目につくところでは、なぜかbench-stringというベンチでClaspがズバ抜けて速い。
bench-stringの定義はこんな感じ

(defun bench-strings (&optional (size 1000000) (runs 50))
  (declare (fixnum size))
  (let ((zzz (make-string size :initial-element #\z))
        (xxx (make-string size)))
    (dotimes (runs runs)
      (and (fill xxx #\x)
           (replace xxx zzz)
           (search "xxxd" xxx)
           (nstring-upcase xxx))))
  (values))

全体的な印象としては、依然としてClaspはまだまだ開発中という感じがした。
ClaspはC++との連携が最大の強みだと思うが、自分はC++の資産を使うこともないので、UTF-8をサポートするまで様子見でも良いかなというところ。
ちなみに、個人的には、SICLのコードが全面的に使われるらしいというところに興味がある。現状は、まだまだECLのコードが多い様子。

下記にベンチ結果を載せてみる。
なお、ECLやClaspと同じくSBCLはGMPを有効にしてある。 使用したマシンは、CPUが、Intel(R) Xeon(R) CPU E3-1230 v3 @ 3.30GHz でメモリは32GiB

ベンチ


HTML generated by 3bmd in LispWorks 7.0.0

再帰的に自己を一回だけインライン展開する

Posted 2017-10-29 23:34:38 GMT

日立が開発していたメインフレーム用Common LispであるHiLISP(VOS3 LISP)についての記事・知識処理用言語HiLISPの高速化方式 — 日立評論 1987年3月号を読んでいて、再帰呼び出しを一回だけインライン展開するという方法が紹介されているのを目にした(記事では、自己再帰展開と呼んでいる)。

面白そうなのでこの自己再帰展開™というものを再現してみることにした。

最近のCommon Lispの処理系ではインライン指定すれば再帰的に展開してくれるものもあれば、そうでないものもあり、展開の仕方もまちまちだけれどもfletで関数内関数を定義し、それにインライン指定すれば簡単に実現できると思われる。

(defmacro defsubstself (name (&rest args) &body body)
  `(defun ,name (,@args)
     (flet ((,name (,@args)
              ,@body))
       (declare (inline ,name))
       ,@body)))

(defsubstself fib/ (n)
  (declare (optimize (speed 3) (safety 0) (debug 0)))
  (declare (fixnum n))
  (the fixnum
       (if (< n 2)
           n
           (+ (fib/ (1- n))
              (fib/ (- n 2))))))
===>
(defun fib/ (n)
  (flet ((fib/ (n)
           (declare (optimize (speed 3) (safety 0) (debug 0)))
           (declare (fixnum n))
           (the fixnum (if (< n 2) n (+ (fib/ (1- n)) (fib/ (- n 2)))))))
    (declare (inline fib/))
    (declare (optimize (speed 3) (safety 0) (debug 0)))
    (declare (fixnum n))
    (the fixnum (if (< n 2) n (+ (fib/ (1- n)) (fib/ (- n 2)))))))

fletの関数定義部のボディの中で自己を参照することはないので、一回だけ展開させるには都合が良い。

自己再帰展開™fibと通常のfibを比べるとLispWorksでは25%程高速化した。
めでたしめでたし。

解決かと思ったが……

fletを使えばできると思ったが、しかし、大域関数をfuncallした場合は、どうなるだろう。
もちろんfletで作った関数は大域関数ではないので呼ぶことはできない。
具体的には下記のような場合。

(defun fib (n)
  (declare (optimize (speed 3) (debug 3) (safety 0)))
  (declare (fixnum n))
  (the fixnum
       (if (< n 2)
           n
           (+ (funcall 'fib (1- n))
              (funcall 'fib (- n 2))))))

そもそも、こういう場合は、インライン展開はどうなるのだろうか。
インライン展開してくれそうな処理系で調べてみると、fibinline宣言がされた場合の挙動は、

処理系 funcall 'fn funcall #'fn
LispWorks 展開する 展開する
SBCL 展開しない 展開する
Allegro CL 展開しない 展開しない

という風に、展開したりしなかったりの様子。
funcall 'fnでもコンパイル時にはインライン指定のスコープはfuncall #'fnと同様に確定できると思われるので、SBCLが展開しないのはちょっと不思議。

3.2.2.3 Semantic Constraints にも

Within a function named F, the compiler may (but is not required to)
assume that an apparent recursive call to a function named F refers to
the same definition of F, unless that function has been declared
notinline. The consequences of redefining such a recursively defined
function F while it is executing are undefined.

とあるので、コンパイル時に確定はできそうだけれど、an apparent recursive call to a function named Ffuncall 'fn形式が含まれるのかは良く分からない。
(functionの定義からして、funcall #'functionは含まれるだろう)
もしかすると、LispWorksがやりすぎなのかもしれない。

ちなみに、Allegro CLはインラインについて一家言あるようなので展開しないらしい。

LispWorksでは、funcall 'fnでもインライン展開するので、大域の補助関数を作成して、それを呼び出せば良さそうだけれど、SBCLではそれでは駄目なので、結局はコードウォーキングしないといけないらしい。

ということで処理系に備わっているwalk-formを使って下記のようなものを書いてみた。

(import (find-symbol (string '#:walk-form)
                     #+allegro :excl
                     #+sbcl :sb-walker
                     #+lispworks :walker))

(defun replace-fn (fsym replace form env) (let ((mark (gensym "mark"))) (subst replace mark (walk-form form env (lambda (sub cxt env) (declare (ignore cxt env)) (when (and (consp sub)) (when (eq fsym (car sub)) (setf (car sub) mark)) (when (and (eq 'function (car sub)) (eq fsym (cadr sub))) (setf (cadr sub) mark)) (when (or (eq 'funcall (car sub)) (eq 'apply (car sub))) (when (and (eq 'quote (caadr sub)) (eq fsym (cadadr sub))) (setf (caadr sub) 'function) (setf (cadadr sub) mark)))) sub)))))

(defmacro defsubstself (name (&rest args) &body body &environment env) (replace-fn name `(lambda (,@args) ,@(copy-tree body)) `(defun ,name (,@args) ,@body) env))

これは、自分の関数名の所をlambdaで置き換えてしまうので下記のような展開になる。
(なお、一度シンボルで置き換えてからlambdaフォームに直しているのは、walk-formが置き換えたフォームを更に展開し展開が止まらなくなるため。)

追記(2018-02-20)

walk-formがとる関数引数は多値を返すことになっていて、Tが返れば、それ以上展開しない、という指定が可能だった。
展開の指定をすれば、後でマーカーを置換するようなことはしなくても良い。
なお、大抵の処理系に付属のwalk-formはPortable CommonLoops(PCL)由来のものだが、この仕様は共通している。

(defun replace-fn (fsym replace form env) 
  (walk-form form
             env 
             (lambda (sub cxt env &aux stopp)
               (declare (ignore cxt env))
               (when (and (consp sub))
                 (when (eq fsym (car sub))
                   (setf (car sub) replace)
                   (setq stopp T))
                 (when (and (eq 'function (car sub))
                            (eq fsym (cadr sub)))
                   (setf (cadr sub) replace)
                   (setq stopp T))
                 (when (or (eq 'funcall (car sub))
                           (eq 'apply (car sub)))
                   (when (and (eq 'quote (caadr sub))
                              (eq fsym (cadadr sub)))
                     (setf (caadr sub) 'function)
                     (setf (cadadr sub) replace)
                     (setq stopp T))))
               (values sub stopp))))

上記では、lambdaで展開してしまったが、fletfibを定義し、funcall 'fibfuncall #'fibに書き換えても良いと思う。

(defsubstself fib (n)
  (declare (optimize (speed 3) (safety 0) (debug 0)))
  (declare (fixnum n))
  (the fixnum
       (if (< n 2)
           n
           (+ (funcall 'fib (1- n))
              (funcall 'fib (- n 2))))))
===>
(defun fib (n)
  ...
 (the fixnum
      (if (< n 2)
          n
          (+ (funcall #'(lambda (n)
                          (declare (optimize
                                    (speed 3)
                                    (safety 0)
                                    (debug 0)))
                          (declare (fixnum n))
                          (the fixnum
                               (if (< n 2)
                                   n
                                   (+ (funcall 'fib
                                               (1- n))
                                      (funcall 'fib
                                               (- n
                                                  2))))))
                      (1- n))
             (funcall #'(lambda (n)
                          (declare (optimize
                                    (speed 3)
                                    (safety 0)
                                    (debug 0)))
                          (declare (fixnum n))
                          (the fixnum
                               (if (< n 2)
                                   n
                                   (+ (funcall 'fib
                                               (1- n))
                                      (funcall 'fib
                                               (- n
                                                  2))))))
                      (- n 2)))))))

こうするとSBCLでも30%程の高速化となった。
ちなみに、Allegro CLだと普通に書いたものより遅くなるため、余計なことはしない方が良いらしい。

結び

HiLISPがfuncall 'fnをどう解釈するのかは分からないので、展開する場合としない場合を考えてみた。

Common Lispではインライン指定は処理系によって任意に解釈できることは知っていたが、調べてみると結構ばらばらなんだなと思った次第。
ちなみにSBCLでは、再帰的な局所関数ではlabels+inline指定をすると展開がかなり効く模様。
なお、大抵の処理系では、今回のような手作りの展開で速くなるが、Allegro CLの場合は、別の作法があるらしく、寧ろずっと遅くなるので注意。


HTML generated by 3bmd in LispWorks 7.0.0

cadadadadddrはなんと読んだらよいのか

Posted 2017-10-24 19:22:07 GMT

Common Lispでは carからcddddrまで30のcarcdrの組み合わせが備え付けで定義されているが、Common Lispプログラマは、caadarのようなものを一体どのように発音しているのだろうか。

このブログのドメイン名は、cddddr.orgだが、くだだだ・だーと日本語の語感として口にしやすいので選んだりしたのだが、dの連続は良いとしても、aの連続や、cdrcdarの違い等はどう表現したら良いのか良くく分からない。

RMSが講演でcaarを「か・あー」と発音していたのを観て、なるほど区切ってみたら案外区別が付くかもなと思い、Common Lispの備え付けに日本語的な発音を付けてみた。

区切りは、a→aとd→aに遷移する時に付けると区別がつきやすいように思う。
また、aが連続する場合は、一番目と二番目の間だけ区切りをはっきりさせると発音しやすい。

下記では区切りを促音にしているが、長音にしても良いだろう。

なお、カダーはキャダーという人もいるようだ(RMSもキャダーと発音していた)。
また、RG(グリーンブラット)は、CADRマシンをカダーと発音していた。同じMITでもまちまちらしい。

  • car: かー
  • cdr: くだー
  • caar: かっあー
  • cadr: かだー
  • cdar: かっだー
  • cddr: くだだー
  • caaar: かっああー
  • caadr: かっあだー
  • cadar: かだっあー
  • caddr: かだだー
  • cdaar: くだっああー
  • cdadr: くだっあだー
  • cddar: くだだっあー
  • cdddr: くだだだー
  • caaaar: かっあああー
  • caaadr: かっああだー
  • caadar: かっあだっあー
  • caaddr: かっあだだー
  • cadaar: かだっああー
  • cadadr: かだっあだー
  • caddar: かだだっあー
  • cadddr: かだだだー
  • cdaaar: くだっあああー
  • cdaadr: くだっああだー
  • cdadar: くだっあだっあー
  • cdaddr: くだっあだだー
  • cddaar: くだだっああー
  • cddadr: くだだっあだー
  • cdddar: くだだだっあー
  • cddddr: くだだだだー

この法則でいくと、本題名のcadadadadddrは、かだっあだっあだっあだだだーと読めることになる。 以上、おそまつ。


HTML generated by 3bmd in LispWorks 7.0.0

Lem使ってみた

Posted 2017-10-21 11:04:10 GMT

Common Lisp製のEmacs系エディタのlemがOpen Collectiveに参加したとのことで、自分も支援してみた。
自分はLispWorksを利用していて、折角LispWorksを購入したからには元を取ろうという貧乏くさい考えで、この二年位は殆どCommon Lispのコードは元より普段の職場での仕事でもLispWorksのエディタでテキストを編集している。

ということで、lemは使ったことがなかったのだが、折角なので使ってみた。

導入

とりあえず、GitHub: cxxxr: lemからソースを持ってきて、Quicklispがロードできる場所に配置。
自分は、Common Lisp処理系内部から使う派なので、あとは、(ql:quickload :lem)して、Common Lisp処理系をダンプするかすることにした。
ちなみに残念ながら現状LispWorks 7.0では上手く動かないらしい。後でちょっとみてみようかなと思う。

使ってみる

起動は、(lem:lem)。伝統のed関数から呼び出すようにしても良さそう。

自分的に必須コマンドである()を対で入力してくれるコマンド(make-())と、コッカからの移動コマンド(move-over-))がなかったので追加してみた。
lemの所作が良く分からないが、とりあえず動けば良いかなという感じ。
ちなみにこれらコマンドは1970年代のEmacsから存在している。

;; -*- lisp -*-
(ql:quickload :g000001.tools.tpd-blog)

(in-package :lem)

(define-key *global-keymap* "C-_" 'undo)

(deftype whitechar () '(member #\Space #\Tab #\Return #\Newline))

(define-command make-\(\) (n) ("p") (let ((cp (current-point))) (insert-character cp #\( n) (insert-character cp #\) n) (prev-char n)))

(define-key *global-keymap* "M-(" 'make-\(\)) (define-key *global-keymap* "M-L" 'make-\(\))

(defun backward-search-rper () (save-excursion (do* ((p (character-offset (current-point) -1)) (c (character-at p) (character-at p))) ((char= #\) c) p) (unless (typep c 'whitechar) (return nil)) (character-offset p -1))))

(defun backward-delete-to-rper () (save-excursion (do* ((p (character-offset (current-point) -1)) (c (character-at p) (character-at p))) ((char= #\) c) p) (unless (typep c 'whitechar) (return nil)) (delete-character p) (character-offset p -1))))

(define-command move-over-\) () () (let ((rper (backward-search-rper))) (if rper (progn (backward-delete-to-rper) (scan-lists (current-point) 1 1 T) (lem.language-mode:newline-and-indent 1)) (progn (scan-lists (current-point) 1 1 T) (lem.language-mode:newline-and-indent 1)))))

(define-key *global-keymap* "M-)" 'move-over-\)) (define-key *global-keymap* "M-:" 'move-over-\))

(define-command Process-Entries-And-Preview (p) ("p") (declare (ignore p)) (g000001.tools.tpd-blog:process-entries-and-preview (buffer-filename (current-buffer)) #P"/mc/"))

(define-command Publog (p) ("p") (declare (ignore p)) (g000001.tools.tpd-blog:publish-entries-and-preview (buffer-filename (current-buffer))))

(define-command Insert-UNIVERSAL-TIME (p) ("p") (declare (ignore p)) (let ((ut (get-universal-time))) (multiple-value-bind (s m h d mo y) (decode-universal-time ut) (declare (ignore s)) (insert-string (current-point) (format nil "~D ;~D-~2,'0D-~2,'0DT~2,'0D~2,'0D" ut y mo d h m)))))

;;; *EOF*

むすび

このブログはLispWorksのエディタから更新できるようにしていたが、何故か無駄に更新コマンドに可搬性を持たせて作成していたので、LispWorksのコマンドをちょっと変更するだけでlemからもこのブログを更新できるようになった。
ということで、この記事も記念にlemで書いてlem上から更新を実行してみた。

また、TwitterもLispWorksからしているが、これもちょっとしたコマンドの書き直しでlem上から簡単につぶやけるようになった。
基本的にエディタ側で作り込むのではなくCommon Lisp側で完結するツールを作成し、フロントエンドはほぼ呼び出すだけの構成にしておくとSLIME・lem・LispWorks等エディタで共通で使いまわせるので良いかもしれない。

lemは現在ターミナルで動くが、今後ブラウザ上や、Electron化も検討されているらしいので色々期待している。


HTML generated by 3bmd in SBCL 1.4.0

SAILDART Lispお宝発掘隊 (1)

Posted 2017-10-15 02:33:20 GMT

前回は、

  • [TCH,LSP]
  • [DWP,LSP]
  • [LAP,LSP]
  • [WD,LSP]

を眺めた。
今回も引き続き下から順番に確認していくことにしよう。

[STR,LSP]

Common Lispの国際標準化についての議論が行なわれていたメーリングリスト。
日本やヨーロッパ勢も参加し、ANSIからISOの流れに繋がっていく。

またこれもmhonarcでhtml化してみた。

[PLC,LSP]

日本の奥乃博先生が中心となって開催された、第三回LISPコンテストおよび第一回PROLOGコンテストのファイル
コンテストは各種ベンチマークの性能を比べる。
何故ここにあるのか謎だが、奥乃先生がSAILにいらしたのと関係があるのかもしれない。
ProLog Contestの略かと思われる。

参考

[LSC,LSP]

同上でこちらは、LiSp Contestのファイル

[MLI,LSP]

SAILで開発され使われていたALGOL記法のLISPであるMLISPのファイル。
処理系のファイルや、メールサーバーのソースコードっぽいものが点在。

参考

[WHT,LSP]

CLtL1のTeX原稿。TeXのマクロでBOLIO風の記述ができるようにしているらしい。
(BOLIOとはMIT Lispコミュニティで利用されていたマークアップ言語)

ファイルを眺めると、CLtL1はSpice Lispのマニュアルの原稿を上書きして作っていたことが分かる。

CLtL2はHTML化され今でも閲覧できるサイトもあり入手も可能だが、CLtL1はHTML化はされていないので案外貴重。
そのうちHTML化してみたい。

今回はここまで。


HTML generated by 3bmd in LispWorks 7.0.0

SAILDART Lispお宝発掘隊 (0)

Posted 2017-10-02 17:06:24 GMT

SAILとは、Stanford Artificial Intelligence Laboratoryの略でスタンフォードAIラボのこと。
DARTとはDump And Restore Techniqueの略だそうで、1972から1990位まで稼動していたバックアップシステムだそう。
そのアーカイブが10年位前から公開されているのが、SAILDART.ORG
アーカイブの中には、マッカーシー先生や、クヌース先生のホームディレクトリがあったりもするし(非公開)、TeXが生れたシステムでもあるので、TeX関係のファイルも多数ある。
Lisp関係では、Common Lispの仕様策定のメールでの議論は、ここSAILのシステムを中心としていたようで多数のお宝が眠っている(と私のようなマニアは考えている)

以前から発掘に勤しんでいるのだが、一度端から端まで確認してみようかなと思い、ブログに記録を残しことにしてみた次第。

まず、システムのLSPディレクトリがあり、このディレクトリは ざっとこんな感じ

  • [MAC,LSP]
  • [TIM,LSP]
  • [COM,LSP]
  • [CLS,LSP]
  • [206,LSP]
  • [NEW,LSP]
  • [FTL,LSP]
  • [MRS,LSP]
  • [AID,LSP]
  • [VLI,LSP]
  • [T,LSP]
  • [VIO,LSP]
  • [RUT,LSP]
  • [OLD,LSP]
  • [IL,LSP]
  • [LSP,LSP]
  • [SCH,LSP]
  • [CMP,LSP]
  • [BUG,LSP]
  • [LIB,LSP]
  • [QLA,LSP]
  • [X3,LSP]
  • [WHT,LSP]
  • [MLI,LSP]
  • [LSC,LSP]
  • [PLC,LSP]
  • [STR,LSP]
  • [TCH,LSP]
  • [DWP,LSP]
  • [LAP,LSP]
  • [WD,LSP]

ディレクトリの名前からだと、中身がなんだかさっぱり見当がつかないが、とりあえず下から全部確認していくことにしよう。

[WD,LSP]

READ.MEに

THIS DIRECTORY CONTAINS EXPERIMENTAL LISP SYSTEM PROGRAMS WHICH SHOULD
NOT BE DEPENDEN UPON

等々とあるので、WDとは、Working Directoryの略かなんかだろうか。

特に見るべきものもないが、pretty.rutのファイルは、UCI LISPのプリティプリンタの定義かなんかだろうか。

[LAP,LSP]

ここも良く分からないが、UCI LISPっぽい。dfuncというのは、UCI LISPのdefunみたいなもだったと思う。
Scheme等と同じく(dfunc (foo arg)...)という形式なのが面白い。
1973年のファイルなので、この定義形式はSchemeに先行するだろう。

[DWP,LSP]

READ.MEに

All files here are on the LISP UDP as of 5/7/80.
        -rpg-

とあるがまったく意味が不明

MLISPの処理系のソースっぽいものがMIDASで書かれている断片がある。

[TCH,LSP]

Common Lispの仕様策定のグループで標準化について技術的な議論をするメーリングリストのアーカイブが置かれていた場所らしい。 メーリングリストの名前は、cl-technical TCHとはtechnicalということか。

とりあえずmhonarcでまとめてみた。

とりあえず、満足したので今日はここまで。


HTML generated by 3bmd in LispWorks 7.0.0

廃止になった expand-defclass について

Posted 2017-10-01 16:57:10 GMT

defclassで構造体を定義するのってできなかったっけかなあと思い、ちょっと調べてみたが、どうもstructure-classとの統合は未完に終わっているようで、MOPでも構造体の生成についての記述はない様子。

(make-instance (make-instance 'structure-class))

に類することができれば、どうにかなりそうだが、思えば構造体にそのような道具はなく、実行時に定義するならコードを生成してからevalみたいなことになりそうだ。
ちなみに、evalが絡むと大抵

(let ((x 42))
  (defstruct foo
    (x x)
    y
    z))

(make-foo) → #S(foo :x 42 :y nil :z nil)

みたいなことができなくなるので避けたい所。
(但し構造体については上記ができても殆ど意味がない)

構造体の定義については、defstructを書くしかないという結論になったが、それでは、defclassdefstructに展開する方向で考えてみようということで、これに使えそうな、expand-defclassを思い出したので使ってみることにした。

(defmethod clos::expand-defclass ((proto structure-class) 
                                  (metaclass (eql 'structure-class))
                                  name
                                  supers
                                  slots
                                  class-options)
  `(defstruct (,name (:include ,@supers))
     ,@(mapcar (lambda (s) 
                 (if (consp s)
                     `(,(car s) ,(getf (cdr s) :initform))
                     `(,s nil)))
               slots)))

これを使うと下記のように書ける

(defstruct foo 
  x
  y
  z)

(defclass bar (foo) ((a :initform 42) b c) (:metaclass structure-class))

(make-bar) → #S(bar :x nil :y nil :z nil :a 42 :b nil :c nil)

(defclass bar (foo)
  ((a :initform 42)
   b
   c)
  (:metaclass structure-class))

をマクロ展開すると、

(defstruct (bar (:include foo)) (a 42) (b nil) (c nil))

となる。

expand-defclassについて

expand-defclassは、MOPの初期(1987年頃)には存在していたが、定義構文の展開方法については具体的に規定しないことになり、定義構文のマクロ展開メソッドは諸々1988年には消えてしまった。
そのためAMOPには載っていない。

(expand-defclass prototype-instance name superclasses slots options environment)

という総称関数だが、展開がメソッドでカスタマイズできるというのが、なかなか良さそう。
なお、LispWorks版は、environment 引数は取らないが、環境は取り込んでいるので謎なことをしている様子。

なお、初期MOPの仕様にほぼ沿っているexpand-defclassは、調べた限りでは、どうもLispWorksにしか存在しないようだ。
(TI ExplorerのTICLOSにはFlavorsで定義されているexpand-defclassがあったのと、Portable CommonLoopsに仕様が違う同名の関数があった。)

まとめ

Common LispもDylan位に色々統合されてくれていたら良かったのになと思ったりする。
ちなみに、Dylanでは動的さが制御できるので性能を出したい場合は制限をかけるようになっている。

また、Eclipse Common Lispは全面的にOOPで書かれているが、Dylanのようにsealed指定が可能なように拡張されていたようだ。


HTML generated by 3bmd in LispWorks 7.0.0

SETFのFって結局なんなの

Posted 2017-10-01 14:34:54 GMT

今日comp.lang.lispの過去ログを眺めていたら、SETFのFはFunctionのF説を説明している人をみつけた。

The thread suggests both FORM and FIELD, but the original meaning was
FUNCTION :-) When the construct was first suggested it was called
SETFQ for "set with function quoted, everything else evaluated", and
only later abbreviated to SETF. (Source: the Gabriel/Steele "Evolution
of Lisp" paper in HOPL-II).

自分もMITのLispマシングループが元ネタとしたA LISP machine with very compact programsの論文には、FieldやFormという記載はなく、Fといえば、Function位だよなあと思っていたが、Evolution of Lispにも記載があるとのことなので確認してみたら、

Deutsch commented that the special form used here is called SETFQ because 
"it quotes the function and evaluates everything else." 
This name was abbreviated to SETF in Lisp-Machine Lisp. 
Deutsch attributed the idea of dual functions to Alan Kay

と書いてあった。
また、Deutsch氏の論文にも、

A more useful function is (SETFQ (fn arg1 ... argn) newvalue)
which quotes the function name and evaluates everything else. 
This allows RPLACA, for example, to be defined as (LAMBDA (X Y) (SETFQ (CAR X) Y) ).

とあり完全に見逃していたらしい。
この論文でいうFunctionは、CLでいうsetfのaccessorみたいな所。

ちなみに、他の説も挙げておこう。

FはForm説

これはsetfはシンボルだけではなくフォームを取るからなんだろうけど、特に誰かの裏付けはなく、なんとなくの皆の印象という感じだろうか。

FはField説

色々な所を眺めると、Kent Pitman氏が広めた感じだが、Pitman氏もDavid Moon氏から教えてもらったようなので大元はMoon氏らしい。
LISP 原書第3版(I) 13章 構造体 にも同様の説明がある。

ちなみに、Deutsch氏のsetfqの機構はAlan Kay氏のアイデアが源泉とあるが、Lisp界全体でこのアイデアが最初に登場したのは、更に10年程遡って1964年頃のLISP 2だったようだ。
なお、LISP 2はAlgolからヒントを得ている。
LISP 2のS式構文では、

(set (car x) 42)

のように書ける。

まとめ

setfのFはFunctionのFだった。と書けば一行で終わる内容だが、色々書くとこんなに長くなる。

関連リンク


HTML generated by 3bmd in LispWorks 7.0.0

ISLISPにモジュールシステムがないのは何故か

Posted 2017-09-03 23:29:18 GMT

ISLISPではCommon Lispのようなパッケージがなくても大丈夫、という話を耳にした。
プログラムの規模によっては大丈夫なのかもしれないが、ISLISPにもパッケージを導入した実装もあることだし、実際どうなのか調べてみた。

ちなみに、パッケージが付いたISLISPであるTISLのサイトは消滅しているので下記にarchive.orgのリンクを貼っておく

また、TISLのパッケージシステムについては下記の論文が一番詳しいようだ。

ISLISPとモジュールシステム

とりあえず、調べて分かったことをまとめてしまうと

  • ISLISPは産業利用が期待されており、モジュールシステムは必須と策定委員会も考えていた。
  • 必須と考えてはいたものの、まとめ切れなかったので見送りとした。

ということらしい。

LISP言語国際標準化と日本の貢献 にはISLISPの成立までの流れが詳細に書いてあるが、Common Lispのパッケージシステムが採用されなかった理由としては、

  • Common Lispのパッケージはデザインが古い

    • パッケージ間の名前の共有関係の静的チェックが困難
    • 名前の隠蔽をすることでの抽象化が上手く実現できない

あたりが述べられている。

Common Lispのパッケージシステムのデザインが良くないので、ISLISPに取り込まれず、ISLISPは将来の課題としたが、宙に浮いてしまった、というところだろうか。

ISLISPの話に、突然Common Lispが出てくるのに違和感があるかもしれないが、こちらもLISP言語国際標準化と日本の貢献を読めばISLISPがCommon Lispのサブセット的な面が強いことが分かると思う。
そもそもCommon Lispが成功したので、ANSIやISOで標準化しようという話になったが、ISO Common Lispが色々あって失敗し、ISLISPに至るという話でもある。

まとめ

ANSI Common Lispにしろ、ISLISPにしろ、時期尚早として見送ったものの、色々あってその時期は永遠にやってこなさそう、という話は多い気がする。


HTML generated by 3bmd in LispWorks 7.0.0

マクロ文字で絵文字を使おう

Posted 2017-08-22 16:34:42 GMT

Perl6は以前から演算子などにUnicodeにASCII外の文字を積極的に使っているが、今回アトミックな操作の演算子として⚛がはいったらしい。

こんな感じのコードらしいが、環境によっては絵文字になる様子。

my atomicint $i = 0;
start { $i ⚛= 1 }
while ⚛$i == 0 { }

ということで、マクロ文字でこういう拡張が簡単にできるCommon Lispでも早速真似してみたい。

(flet ((|read-🤙| (srm chr)
         (declare (ignore chr))
         (cl:quote cl:funcall))
       (|read-λ| (srm chr)
         (declare (ignore chr))
         (cl:quote cl:lambda)))
  (set-macro-character #\🤙 #'|read-🤙|)
  (set-macro-character #\λ #'|read-λ|))

(🤙(🤙(λ (f) ((λ (proc) (🤙f (λ (arg) (🤙(🤙proc proc) arg)))) (λ (proc) (🤙f (λ (arg) (🤙(🤙proc proc) arg)))))) (λ (f) (λ (n) (if (< n 2) n (+ (🤙f (1- n)) (🤙f (- n 2))))))) 10) → 55

Unicode 9.0 に Call Me Hand “🤙” (U+1F919) という丁度良いのがあったので使ってみた。

🤙 が開き括弧っぽいので、もう一捻りして、

(flet ((|read-🤙| (srm chr)
         (declare (ignore chr))
         (cons (quote funcall)
               (read-delimited-list #\) srm T)))
       (|read-λ| (srm chr)
         (declare (ignore chr))
         (cl:quote cl:lambda)))
  (set-macro-character #\🤙 #'|read-🤙|)
  (set-macro-character #\λ #'|read-λ|))

🤙🤙(λ (f) ((λ (proc) 🤙f (λ (arg) 🤙🤙proc proc) arg)))) (λ (proc) 🤙f (λ (arg) 🤙🤙proc proc) arg)))))) (λ (f) (λ (n) (if (< n 2) n (+ 🤙f (1- n)) 🤙f (- n 2))))))) 10) → 55

こんなのもどうだろうか。これならLisp-1と文字数は一緒になる。
とはいえ、ここまで来るとエディタにも追加設定して追従させないといけないが。 (Emacsなら括弧の文字設定が可能なので多分大丈夫だろう)


HTML generated by 3bmd in LispWorks 7.0.0

Common Lispを実装するのに再利用できそうなコンポーネント群

Posted 2017-08-21 16:29:38 GMT

たった一人でCommon Lispの処理系を作るというのはあまり耳にしないけれど、そんな人も皆無ではない。
たとえば、XCLや、Eclips CL
どちらももう開発は停止しているが、Eclips CLは、ANSI CL以降に実装されただけあって、処理自体がANSI CL以降のスタイルでOOP機能を使って書かれたりもしている。そしてMOP付き。

一人でMOP装備の処理系を開発しているというのは極端だけれど、チームで開発している人達も、大抵、既存のコンポーネントは再利用していることが殆ど。
Common Lispの処理系のコンポーネントの配布もまたかなり昔からある。

一番古い所では、Spice Projectが配布していたもので、1981・2年から存在する。
京大で独自に実装されたためKCLがこのキットを使っていなかったことに当時のCommon Lisp実装者達が驚いたのは有名(でもない)
ちなみに、Spice Projectのキットを使っていたものには、TOPS-20 Common Lispや商用のVAX LISPなども存在する。

また、オブジェクト指向システムでは、Portable CommonLoops(PCL)が参照実装として流通していて、なんだかんだで現在開発が活発なCommon Lisp処理系はどれもこれをベースにしている。

LOOPの実装は大抵の処理系は、MIT LOOPのバージョン829を元にしている。
MIT LOOPが大半なのでMIT LOOP依存なコードが発生してしまっているほど。

Common Lispからブートストラップする処理系としては、上述したHoward Stearns氏のEclisp CL、Robert Strandh氏のSICL、峯島氏のSacraがあり、Eclipse CLは商用処理系として販売もされていた。
SICLは規格に準拠したより綺麗な実装を目指していて、当該プロジェクト以外でもClaps等で、コンポーネントが利用されている。
Sacraは、XCLで部分的に利用されたりしている。

その他Thinlisp等トランスレータ系のソースもあり、さらにマニアックな所では、古いLispマシンのソースや、Lucid CLのソースも入手可能。
とはいえこの辺りはライセンスが微妙。(一応CADRは、MIT Licenseだが)

また、CLISPが結構独自実装なのでコードの再利用性は高いかもしれない。

以上、なんのまとまりもないがリンクを並べておく


HTML generated by 3bmd in LispWorks 7.0.0

CrayでLisp

Posted 2017-08-06 08:23:13 GMT

Cray J90の公開エミュレータサイト現わる

先日、伝説のスパコンで有名なCray社のマシンであるCray J90のエミュレータを動かして公開している酔狂なサイトが登場した。

Cray J90は、Y-MPのエントリーレベルのマシンであるY-MP ELの後継機で1994年に登場したマシンらしい

Cray J90 と Lisp

珍しいマシン環境を見付けたらまずLispが動くかどうかを調べたいところ。
ざっと検索してみたところ、Portable Standard Lisp(PSL)がCray上で稼動していたようだ。
PSLとは、数式処理システムのREDUCEを動かすことを念頭に置いて開発されたStandard Lispの後継で、より可搬性の高い処理系。

Portable Standard Lisp を Cray J90 でビルドする

PSLについても最近の環境でビルドできるようにして公開している方がいるので、こちらのソースを利用することにしてみる(とはいえターゲットは1994年の環境だが……)

ちなみに、公開J90マシンへのファイルの転送はftpを利用するが、モードをpassiveにしないと上手く行かなかった(なんとなく懐かしい)

早速、ファイルを転送して展開する。
なお、当該機にgzipはないようなので注意。

makefileがgcc用なので、適当に修正する。

インタプリタのビルドは、make lispで、make lispcでコンパイラのビルドとなるが、インタプリタ環境ファイルとコンパイラ環境ファイルがごっちゃになるので、別々にした方が良いだろう。

とりあえず、make lispしてできたlispを環境が混ざらないように、別のディレクトリを作成し、そこに移動。そして実行してみる。

(de fib (n)
    (cond ((lessp n 2) n)
          (t (plus (fib (difference n 1))
                   (fib (difference n 2))))))

こんな感じのfibの定義を作成してloadさせてみる。

bash-2.03$ ./lisp
No initialization file: LISP-INI or LISP-INI
    S  T  D     L  I  S  P      [7.2] June 2015 
> (load "fib.lsp")
nil
> fib
> !$eof!$
> (fib 10)
55
> 

とりあえず動いた。

なお、コンパイラの方もlispcはできて実行できるのだが、どうも上手く動かせていない。

Common Lisp処理系は動くか

1994年の環境なので、KCL位なら動くかもしれない。
ちなみに、Franzの社史によると、Duane Rettig 氏が Allegro CLをX-MPに移植したとのこと。
Unicosは互換性が高いのでX-MP用のバイナリであれば動きそう。

まとめ

やはりCrayのマシンはロマン。
興味のある方は是非UnicosでCommon Lisp処理系が動くようにして頂きたい。


HTML generated by 3bmd in LispWorks 7.0.0

GambolでZebraベンチ

Posted 2017-06-28 13:06:26 GMT

Gambolとは、FROLICというCommon Lisp実装のS式Prologを拡張したものからProlog部分を抜き出したものらしい。

導入は、Quicklisp経由で、

(ql:quickload :gambol)

とすれば導入できる。

とりあえず、PrologのCommon Lisp実装を見付けたらZebraパズルでベンチをとってみることにしているので、早速いつもの組み合わせでベンチをとってみることにした。
(Allegro CL 8.2 64bit / Intel(R) Xeon(R) CPU E3-1230 v3 @ 3.30GHz)

なお、gambolだと色々試しているうちに定義が混ざるので定義の度にリセットするようなマクロを書いた。
また、gambolでは、匿名変数は??の筈だが、??だとどうも上手く動かないので、#?というリーダーマクロで一意なシンボルを生成した。

(cl:in-package :cl-user)

(eval-when (:compile-toplevel :load-toplevel :execute) (ql:quickload :gambol))

(defpackage :gambol-zebra (:use :cl :gambol))

(in-package :gambol-zebra)

(eval-when (:execute :compile-toplevel :load-toplevel) (defvar *gambol-zebra-readtable* (copy-readtable nil)) (set-dispatch-macro-character #\# #\? (lambda (s c a) (declare (ignore s c a)) (gensym "?")) *gambol-zebra-readtable*) (setq *readtable* *gambol-zebra-readtable*))

(defmacro define-predicate (name &body clauses) `(progn (clear-rules '(,name)) ,@(mapcar (lambda (c) `(*- ,@c)) clauses)))

(define-predicate member ((member ?item (?item . #?))) ((member ?item (#? . ?rest)) (member ?item ?rest)))

(define-predicate nextto ((nextto ?x ?y ?list) (iright ?x ?y ?list)) ((nextto ?x ?y ?list) (iright ?y ?x ?list)))

(define-predicate iright ((iright ?left ?right (?left ?right . ?rest))) ((iright ?left ?right (?x . ?rest)) (iright ?left ?right ?rest)))

(define-predicate zebra ((zebra ?h ?w ?z) ;; Each house is of the form: ;; (house nationality pet cigarette drink house-color) (= ?h ((house norwegian #? #? #? #?) ;1,10 #? (house #? #? #? milk #?) #? #?)) ; 9 (member (house englishman #? #? #? red) ?h) ; 2 (member (house spaniard dog #? #? #?) ?h) ; 3 (member (house #? #? #? coffee green) ?h) ; 4 (member (house ukrainian #? #? tea #?) ?h) ; 5 (iright (house #? #? #? #? ivory) ; 6 (house #? #? #? #? green) ?h) (member (house #? snails winston #? #?) ?h) ; 7 (member (house #? #? kools #? yellow) ?h) ; 8 (nextto (house #? #? chesterfield #? #?) ;11 (house #? fox #? #? #?) ?h) (nextto (house #? #? kools #? #?) ;12 (house #? horse #? #? #?) ?h) (member (house #? #? luckystrike oj #?) ?h) ;13 (member (house japanese #? parliaments #? #?) ?h) ;14 (nextto (house norwegian #? #? #? #?) ;15 (house #? #? #? #? blue) ?h) (member (house ?w #? #? water #?) ?h) ;Q1 (member (house ?z zebra #? #? #?) ?h) ;Q2 ))

(defun zebra-benchmark (&optional (n 1000)) (declare (optimize (speed 3) (safety 0))) (let (rt0 rt1) (time (loop :initially (setf rt0 (get-internal-run-time)) :repeat n :do (pl-solve-one '((zebra ?h ?w ?z))) :finally (setf rt1 (get-internal-run-time)))) (multiple-value-call #'values (/ (* n 12825) (/ (- rt1 rt0) 1000.0)) (values-list (pl-solve-one '((zebra ?h ?w ?z)))))))

結果

結果は、82秒とPAIPrologの8倍遅い結果となった。
SBCLだと18秒、LispWorksだと30秒程度なので、Allegro CLと相性が良くないのかもしれない。
また、Allegro CLとLispWorksでは、スタックが溢れるので、

#+allegro (sys:set-stack-cushion nil)
#+lispworks (setq sys:*stack-overflow-behaviour* nil)

等と処置する必要があるかもしれない。

(gambol-zebra::zebra-benchmark 1000)
; cpu time (non-gc) 44.780000 sec user, 0.000000 sec system
; cpu time (gc)     38.100000 sec user, 0.000000 sec system
; cpu time (total)  82.880000 sec (00:01:22.880000) user, 0.000000 sec system
; real time  82.874676 sec (00:01:22.874676)
; space allocation:
;  263,790,840 cons cells, 9,367,684,480 other bytes, 0 static bytes
154741.8
→ (?h
   (house norwegian fox kools water yellow)
   (house ukrainian horse chesterfield tea blue)
   (house englishman snails winston milk red)
   (house spaniard dog luckystrike oj ivory)
   (house japanese zebra parliaments coffee green)) 
  (?w . norwegian) 
  (?z . japanese) 

結び

PrologのCommon Lisp実装は他にも結構あるらしいので、見付けたらZebraパズルを試していきたい。


HTML generated by 3bmd in LispWorks 7.0.0

Common LispのScreamerでZebraベンチ

Posted 2017-06-13 16:22:44 GMT

Screamerとは古くからあるCommon Lispの非決定性計算のライブラリで、知る人ぞ知るという感じのものだが、現在もQuicklisp経由で簡単に導入することが可能。

(ql:quickloda :screamer)

数年前ScreamerでZebraパズルを記述したものがあったので試してみたが、どうも遅いっぽいなあという漠然とした印象だけ残っていた。
最近Zebraパズルばっかりやっているが、無駄な知見が溜りつつある丁度良いタイミングなので果して本当に遅かったのか確認してみることにした(暇ともいう)

筆者が以前目にしたものは、SBCLの開発で有名な、Nikodemus Siivola氏が書いたものだったらしい。

こちらのコードを少しSWI-Prologのコードっぽくして、計時してみた。

(declaim (optimize (speed 3) (safety 0) (debug 0) (compilation-speed 0)))

(eval-when (:compile-toplevel :load-toplevel :execute) (ql:quickloda :screamer))

(in-package :s)

(defmacro let-integers-betweenv (((min max) var-list) body) `(let ,(loop for i in var-list collect (list i `(an-integer-betweenv ,min ,max))) ,body))

(defun all-differentv (list) ;; Functionally the same as (apply #'/=v list), but faster. (labels ((all-different (x xs) (if (null xs) t (andv (notv (=v x (car xs))) (all-different x (cdr xs)) (all-different (car xs) (cdr xs)))))) (all-different (car list) (cdr list))))

(defun nextto (x y) (let ((d (an-integer-betweenv -1 1))) (assert! (/=v d 0)) (assert! (=v d (-v x y)))))

(defun iright (x y) (assert! (=v x (+v y 1))))

(defun zebra-problem () (let-integers-betweenv ((1 5) (english spaniard japanese ukrainian norwegian red green ivory yellow blue dog snails fox horse zebra kools chesterfield winston luckystrike parliament tea coffee milk oj water z w)) (let ((nationality (list english spaniard japanese ukrainian norwegian)) (color (list red green ivory yellow blue)) (smoke (list kools chesterfield winston luckystrike parliament)) (pet (list zebra dog snails fox horse)) (drink (list water tea coffee milk oj))) (assert! (all-differentv nationality)) (assert! (all-differentv color)) (assert! (all-differentv smoke)) (assert! (all-differentv pet)) (assert! (all-differentv drink)) ;; (assert! (=v norwegian 1)) (assert! (=v milk 3)) (assert! (=v english red)) (assert! (=v spaniard dog)) (iright green ivory) (nextto norwegian blue) (assert! (=v kools yellow)) (assert! (=v green coffee)) (assert! (=v ukrainian tea)) (assert! (=v luckystrike oj)) (assert! (=v japanese parliament)) (assert! (=v winston snails)) (assert! (=v z zebra)) (assert! (=v w water)) (nextto horse kools) (nextto fox chesterfield)

(destructuring-bind (z w &rest result) (one-value (solution (list z w nationality pet drink color smoke) (static-ordering #'linear-force))) (let* ((syms '((english spaniard japanese ukrainian norwegian) (zebra dog snails fox horse) (water tea coffee milk oj) (red green ivory yellow blue) (kools chesterfield winston luckystrike parliament) )) (result (apply #'mapcar #'list (mapcar (lambda (x) (mapcar #'second (sort x #'< :key #'first))) (mapcar (lambda (x y) (mapcar #'list x y)) result syms))))) (list (nth 0 (nth (1- z) result)) (nth 0 (nth (1- w) result)) result))))))

(zebra-problem)(japanese norwegian ((norwegian fox water yellow kools) (ukrainian horse tea blue chesterfield) (english snails milk red winston) (spaniard dog oj ivory luckystrike) (japanese zebra coffee green parliament)))

Allegro CL 8.2 64bitで大体210秒位。オリジナルも大体同じ位のスピードで、assert!の節を関数として括り出しても性能的には問題ないようだ。
ちなみに、LispWorksや、SBCLだと130秒位で終了するので、コードの最適化がSBCLやLispWorksに向けて施されているかもしれない。

;(time (dotimes (i 1000) (zebra-problem)))
; cpu time (non-gc) 197.770000 sec (00:03:17.770000) user, 0.010000 sec system
; cpu time (gc)     12.670000 sec user, 0.000000 sec system
; cpu time (total)  210.440000 sec (00:03:30.440000) user, 0.010000 sec system
; real time  210.561596 sec (00:03:30.561596)
; space allocation:
;  74,892,808 cons cells, 70,951,714,176 other bytes, 0 static bytes)

コードの中身としては、これまでのリスト処理のコードとはちょっと違っていて、各要素に1から5までの数値を割り当て、要求された条件に合う解を見付けてくるようなものになっている。
Prologに比べると変数の初期化等のコード量が多く、その辺りの見通しが少し良くない。

なおオリジナルのコードのコメントによると、もう少し速くなる書き方があるらしい。
20倍位速くなればPAIProlog並ということになるが、ここまで手続的に書いてPAIPrologより遅いとなるとZebraのような問題に限っては、組み込みPrologを使った方が楽で良いなと思ってしまう。

結び

いかつい名前から勝手に超高速なものという印象を持っていたが、Zebraに関しては若干期待外れだった。

Screamerが得意とする問題もあると思うので、得意なものもそのうち確認してみたい。


HTML generated by 3bmd in LispWorks 7.0.0

Common LispのminiKANRENでZebraベンチ

Posted 2017-06-09 19:09:28 GMT

Common Lisp上から使える論理型言語・DSLを適当に眺めたりしているが、今回はminiKANRENを試してみる。

miniKANRENとは

KANRENはScheme上に実装された論理型・関係型言語で、名前は日本語の関連(relation)に由来するらしい。
元々miniKANRENはそのサブセットだったようだが、今では一つの流派を成しているようだ。

実装がシンプルなので多数の言語の上で稼動する。最近のもので比較的有名なものとしてClojureのcore.logicがある。

Prologと比較すると、より関数型言語との親和性が高かったり、Occur Checkがあったり、基本的にcutはなかったり色々と違うようだ。

Zebraベンチを走らせてみる

Common LispにもminiKANRENは移植されていて、quicklisp経由で導入することができる。

(ql:quickload :kanren-trs)

とりあえず、毎度試しているSWI-Prolog版のZebraベンチのコードをminiKANRENで書いてみた。

(defpackage :k
  (:use :cl :kanren-trs))

(defun memb (item list) (fresh (a d) (conde ((== '() list) +fail+) ((== (cons item d) list)) ((== (cons a d) list) (memb item d)))))

(defun nextto (x y list) (conde ((iright x y list)) ((iright y x list))))

(defun iright (left right list) (fresh (a d r) (conde ((== '() list) +fail+) ((== (cons a '()) list) +fail+) ((== (cons left d) list) (== (cons right r) d)); left d:(right r) ((== (cons a d) list) (iright left right d)))))

(defun replace-_ (tree) (let ((vars '())) (labels ((frob (tree) (typecase tree (null '()) (atom tree) (cons (case (car tree) (_ (let ((s (gensym))) (push s vars) (cons s (frob (cdr tree))))) (otherwise (cons (frob (car tree)) (frob (cdr tree))))))))) (values (frob tree) vars))))

(defmacro fresh* (&body body) (multiple-value-bind (newbody vars) (replace-_ body) `(fresh (,@vars) ,@newbody)))

(defun zebra (h w z) (fresh* (== h `((norwegian ,_ ,_ ,_ ,_) (,_ ,_ ,_ ,_ ,_) (,_ ,_ ,_ milk ,_) (,_ ,_ ,_ ,_ ,_) (,_ ,_ ,_ ,_ ,_))) (memb `(englishman ,_ ,_ ,_ red) h) (memb `(spaniard dog ,_ ,_ ,_) h) (memb `(,_ ,_ ,_ coffee green) h) (memb `(ukrainian ,_ ,_ tea ,_) h) (iright `(,_ ,_ ,_ ,_ ivory) `(,_ ,_ ,_ ,_ green) h) (memb `(,_ snails winston ,_ ,_) h) (memb `(,_ ,_ kools ,_ yellow) h) (nextto `(,_ ,_ chesterfield ,_ ,_) `(,_ fox ,_ ,_ ,_) h) (nextto `(,_ ,_ kools ,_ ,_) `(,_ horse ,_ ,_ ,_) h) (memb `(,_ ,_ luckystrike oj ,_) h) (memb `(japanese ,_ parliaments ,_ ,_) h) (nextto `(norwegian ,_ ,_ ,_ ,_) `(,_ ,_ ,_ ,_ blue) h) (memb `(,w ,_ ,_ water ,_) h) (memb `(,z zebra ,_ ,_ ,_) h)))

KANRENでは匿名関数がサポートされているように見えるが、miniKANRENではサポートされていないらしい。
毎度freshで手書きで指定するのはあまりにも辛いのでfresh*という適当なマクロを書いてみた。なお入れ子での利用は想定していない。

ちなみにマクロで圧縮しないと、下記のようになる。Zebraのように変数が多いとちょっと辛い。

(defun zebra* (h w z)
  (declare (optimize (speed 3) (safety 0) (debug 0)))
  (fresh (a1 a2 a3 a4 a5
          b1 b2 b3 b4 b5
          c1 c2 c3 c4 c5
          d1 d2 d3 d4 d5
          e1 e2 e3 e4 e5)
    (== h `((norwegian ,a2 ,a3 ,a4 ,a5)
            (,b1 ,b2 ,b3 ,b4 ,b5)
            (,c1 ,c2 ,c3 milk ,c5)
            (,d1 ,d2 ,d3 ,d4 ,d5)
            (,e1 ,e2 ,e3 ,e4 ,e5)))
    (fresh (t1 t2 t3)
      (memb `(englishman ,t1 ,t2 ,t3 red) h))
    (fresh (t1 t2 t3)
      (memb `(spaniard dog ,t1 ,t2 ,t3) h))
    (fresh (t1 t2 t3)
      (memb `(,t1 ,t2 ,t3 coffee green) h))
    (fresh (t1 t2 t3)
      (memb `(ukrainian ,t1 ,t2 tea ,t3) h))
    (fresh (t1 t2 t3 t4 t5 t6 t7 t8)
      (iright `(,t1 ,t2 ,t3 ,t4 ivory) `(,t5 ,t6 ,t7 ,t8 green) h))
    (fresh (t1 t2 t3)
      (memb `(,t1 snails winston ,t2 ,t3) h))
    (fresh (t1 t2 t3)
      (memb `(,t1 ,t2 kools ,t3 yellow) h))
    (fresh (t1 t2 t3 t4 t5 t6 t7 t8)
      (nextto `(,t5 ,t6 chesterfield ,t7 ,t8) 
              `(,t1 fox ,t2 ,t3 ,t4) h))
    (fresh (t1 t2 t3 t4 t5 t6 t7 t8)
      (nextto `(,t5 ,t6 kools ,t7 ,t8) 
              `(,t1 horse ,t2 ,t3 ,t4) h))
    (fresh (t1 t2 t3)
      (memb `(,t1 ,t2 luckystrike oj ,t3) h))
    (fresh (t1 t2 t3)
      (memb `(japanese ,t1 parliaments ,t2 ,t3) h))
    (fresh (t1 t2 t3 t4 t5 t6 t7 t8)
      (nextto `(norwegian ,t1 ,t2 ,t3 ,t4)
              `(,t5 ,t6 ,t7 ,t8 blue) h))
    (fresh (t1 t2 t3 t4)
      (memb `(,w ,t2 ,t3 water ,t4) h))
    (fresh (t2 t3 t4)
      (memb `(,z zebra ,t2 ,t3 ,t4) h))))

計時

さて、これで、下記のような感じで、いつもと同じAllegro CL 8.2 64bitで1000回繰り返してみた。

(time 
 (dotimes (i 1000)
   (run nil (a)
     (fresh (h w z)
       (zebra h w z)
       (== a (list h w z))))))

; cpu time (non-gc) 216.919057 sec (00:03:36.919057) user, 0.000000 sec system ; cpu time (gc) 23.884974 sec user, 0.000000 sec system ; cpu time (total) 240.804031 sec (00:04:00.804031) user, 0.000000 sec system ; real time 240.977189 sec (00:04:00.977189) ; space allocation: ; 383,627,240 cons cells, 38,243,671,232 other bytes, 0 static bytes

結果は、216秒とかなり遅かった。PAIPrologの約20倍、AZ-Prolog・Allegro Prologと比較すると約250〜300倍遅い。
Common Lispの実装は特に高速化は施されていないのでこんなものなのかもしれない。

ちなみに今回、ウェブ上で散見されるZebraベンチにも色々なバージョンがあることと、述語の並べ方によって10倍以上の速度の違いが生じることがあることに気付いた。
SWI-Prologのベンチでは、家の情報(全体の情報)、水を飲んでいる物、シマウマの所有者の3つの変数を使うが、全体の情報を取得のみの場合もあるようだ。
また、述語の並べ方としては具体的には、

(defun zebra/ (h w z)
  (fresh*
    (== h `((norwegian ,_ ,_ ,_ ,_)
            (,_ ,_ ,_ ,_ ,_)
            (,_ ,_ ,_ milk ,_)
            (,_ ,_ ,_ ,_ ,_)
            (,_ ,_ ,_ ,_ ,_)))
    (iright `(,_ ,_ ,_ ,_ ivory)
            `(,_ ,_ ,_ ,_ green) h)
    (nextto `(norwegian ,_ ,_ ,_ ,_)
            `(,_ ,_ ,_ ,_ blue) h)
    (memb `(englishman ,_ ,_ ,_ red) h)
    (memb `(spaniard dog ,_ ,_ ,_) h)
    (memb `(japanese ,_ parliaments ,_ ,_) h)    
    (memb `(ukrainian ,_ ,_ tea ,_) h)
    (nextto `(,_ ,_ chesterfield ,_ ,_)
            `(,_ fox ,_ ,_ ,_) h)
    (memb `(,_ snails winston ,_ ,_) h)    
    (memb `(,_ ,_ kools ,_ yellow) h)    
    (memb `(,_ ,_ luckystrike oj ,_) h)
    (memb `(,w ,_ ,_ water ,_) h)
    (memb `(,z zebra ,_ ,_ ,_) h)
    (memb `(,_ ,_ ,_ coffee green) h)
    (nextto `(,_ ,_ kools ,_ ,_) 
            `(,_ horse ,_ ,_ ,_) h)))

のような順番で述語を記述すると、約11秒なので20倍程度は速い。
なお、Allegro Prologなどでもこの述語の並びだと10倍程度速くなるようなので、Zebraベンチの場合は述語の並びを揃えないと、うまく比較できないと考えた方が良いようだ。

結び

miniKANRENは親言語とのデータのやりとりも簡単で使い勝手良さそうだ。
miniKANRENといえば、The Reasoned Schemerらしいので、そのうち読んでみたい。


HTML generated by 3bmd in LispWorks 7.0.0

Boizumault本のMini-Prolog-IIでZebraベンチ

Posted 2017-06-02 15:16:36 GMT

先日退職し、またも無職となったが、同僚から餞別でPatrice Boizumault氏のThe Implementation of Prologを頂いた。
これ前から欲しかったので非常に嬉しい!。ありがたや!!。

この本は、タイトル通りPrologを実装していこうという本で、出版は1993年と古いが、WAMベースなPrologをCommon Lispで実装しようというのが、筆者的には魅力の本。

この本のコードはCMUのAIリポジトリにあるので、本はまだ読んでいないが、とりあえずどんなものか動かしてみることにした。

ファイルを展開すると色々とファイルがあるが、Prologの実装の本なので順を追って複雑になっているらしい。
Mini-Prolog-IIが最終版のようなので、こちらを動かすことにする。

$ cd microPrologII/
$ make

とすると、V4.lspというファイルができる。
540行目付近で;の付け忘れがあるので修正しよう。

544c536
<           (push_cont)                 ; saves current cont.
---
>           (push_cont) saves current cont.

さて、このファイルを実行すると

Mini-PrologII

; Loading text file /l/src/rw/mini-prolog-ii/mlg.Start /l/src/rw/mini-prolog-ii/mlg.Start

| ?-

のように初期化ファイルを読み込んでPrologが開始される。S式PrologではなくDEC-10 Prolog文法らしい。

| ?- conc([a,b,c],Xs,[a,b,c,d,e,f]). % conc = append
xs = [d,e,f]
no More

Zebraベンチを走らせる

とりあえず動いたが、ファイルを読み込ませる方法が分からないので、とりあえずLisp側から読み込ませてみることにした。

(defun mp2-load (file)
  (let ((*readtable* mini-prolog-ii::*mini-prolog-ii-readtable*)
        (*package* (find-package :mini-prolog-ii)))
    (load file)))

(mp2-load "zebra.mpl")

という風に読み込ませる。

ベンチのコードは、下記のようにしたが、これまで測定に利用してきたAllegro PrologのページのものをMini-Prolog-IIで動くように調整したもの。

% -*- Mode: prolog -*-
%
% This file for benchmarking against Mini-Prolog-II.
%

$ member(Item, [Item|_]). $ member(Item, [_|T]) :- member(Item, T).

$ nextto(X, Y, List) :- iright(X, Y, List). $ nextto(X, Y, List) :- iright(Y, X, List). $ iright(Left, Right, [Left, Right | _]). $ iright(Left, Right, [_ | Rest]) :- iright(Left, Right, Rest).

$ zebra(H, W, Z) :- eq(H,[house(norwegian, _, _, _, _), _, house(_, _, _, milk, _), _, _]), member(house(englishman, _, _, _, red), H), member(house(spaniard, dog, _, _, _), H), member(house(_, _, _, coffee, green), H), member(house(ukrainian, _, _, tea, _), H), iright(house(_, _, _, _, ivory), house(_, _, _, _, green), H), member(house(_, snails, winston, _, _), H), member(house(_, _, kools, _, yellow), H), nextto(house(_, _, chesterfield, _, _), house(_, fox, _, _, _), H), nextto(house(_, _, kools, _, _), house(_, horse, _, _, _), H), member(house(_, _, luckystrike, oj, _), H), member(house(japanese, _, parliaments, _, _), H), nextto(house(norwegian, _, _, _, _), house(_, _, _, _, blue), H), member(house(W, _, _, water, _), H), member(house(Z, zebra, _, _, _), H).

% This runs the query a single time: % ?- zebra(Houses, WaterDrinker, ZebraOwner). %

$ zebra1(Houses, WaterDrinker, ZebraOwner) :- zebra(Houses, WaterDrinker, ZebraOwner), !.

$ zebran(X,X). $ zebran(N,Limit) :- zebra1(Houses, WaterDrinker,ZebraOwner), plus(N,1,N1),!, zebran(N1,Limit).

実は、調整したといいつつ、Mini-Prolog-IIは匿名変数の_をサポートしていないらしい。
シンボル名が被らないように記述すれば良いが面倒なので処理系を改造することにした。
といっても簡単な改造で、_シンボルだったら(gensym)するというだけのもの。

(defun read_atom (ch)                   ; next normal symbol
  (do ((lch (list ch) (push (read-char) lch)))
      ((not (alphanum (peek-char)))
       (let ((sym (implode (reverse lch))))
         (if (string= "_" sym)
             (gensym "_")
             sym)))))

これで匿名変数がちゃんとサポートできているのかは良く分からないが、とりあえず上手く動いているようだ。

| ?- zebra1(Houses,WaterDrinker,ZebraOwner).
zebraowner = japanese
waterdrinker = norwegian
houses = [house(norwegian,fox,kools,water,yellow),
          house(ukrainian,horse,chesterfield,tea,blue),
          house(englishman,snails,winston,milk,red),
          house(spaniard,dog,luckystrike,oj,ivory),
          house(japanese,zebra,parliaments,coffee,green)]
no More

計時

備え付けで、cputimeというものがあるが測定しづらいので、timeという単なる時間を取得する述語を作成し時刻差を計算することにする。

繰り返しには、zebranという任意の回数繰り返すものを作成し利用してみた。
しかし、18回以上回すとオーバーフローするので、15回位の繰り返しとし、1000回繰り返した場合の予測とする。

Allegro Prologと比較したいので、計時プラットフォームはAllegro CL 8.2。

time(S),!,
zebran(0,15),!,
time(E),!,
minus(E,S,Time),!.

を実行すると、15回で大体470msなので、1000回回したら31秒という所だろうか。

過去にZebraベンチを同じ条件で計時したことがあるが、PAIPrologが11秒だったのでその3倍位になる。

最適化宣言をして、それだけで速くなったら儲け物なので、次に下記のような宣言をし、

(declaim (optimize (speed 3) (safety 0) (debug 0) (compilation-speed 0)))

再度計時してみたが、これだけで12.4秒位までは速くなった。大体PAIPrologと一緒のタイムだ。

ちなみに、SBCLでは、10秒、LispWorksでは11秒と若干速くなるらしい。

結び

AZ-Prolog並のスピードを出すAllegro Prologは、PAIPのPAIPrologをチューニングしたものがベースになっているが、元のPAIPrologもまあまあ速いようだ。

Mini-Prolog-IIをいじって高速化できたら楽しいので、The Implementation of Prologを読んでPrologの実装について勉強することにしよう。

なお、今回の計時で利用した一式はGitHubに置いてみてある

関連記事


HTML generated by 3bmd in LispWorks 7.0.0

Common Lispのお宅拝見: CMU Common Lisp篇

Posted 2017-06-01 07:09:45 GMT

今回は、CMU Common Lispのcl-userを眺める。

現在でも開発が続いているCommon Lispの処理系の系列としては、CMU Common Lisp(CMUCL)は、1980年あたりのSpice Lispから連綿と続いており、もうすこしで40年になろうとしている。
CMUCLからフォークしたSBCLの方が現在は開発・利用とも活発だが、この系統はCLtL1がSpice Lispのマニュアルを下敷として作成されていたり、Common Lispの歴史と関係が深い。
他にCMUCLからフォークしたものとしては、商用処理系のScieneer Common Lispがあり、こちらはSBCLと同じく64bit化もされている。

cl-userパッケージの構成

さて、cl-userの構成だが、拡張ユーティリティのextentions(ext)パッケージをuseしている。
extパッケージは、便利関数・マルチプロセッシング・拡張機能等で250位の関数・変数が定義されている。

古くからあるものを一つ紹介するとcollectのようなものがある。

(collect ((acc '(-1)))
  (dotimes (i 10)
    (acc i))
  (acc))(-1 0 1 2 3 4 5 6 7 8 9)

また、CMUCLは、double-doubleというdoubleを二つ使って精度を高めた浮動小数点数形式をサポートしているので、そのあたりの定義がある。

(let ((*read-default-float-format* 'double-double-float))
  (read-from-string "3.14159265358979323846264338327950288419716939937511"))
→  3.1415926535897932384626433832795w0
    52

また面白いのが、2000年代位にAllegro CLの機能を取り込んでいて、階層パッケージがあったり、古くからあるencapsulateを土台としてAllegro CL互換のAdvice機構(fwappers)を構築したりしているらしい(fwrappersはextパッケージにはなく別パッケージ)

さて毎度確認しているtruefalseだが、CMUCLにも実装されていなかった。あれれ。

結び

Common Lisp誕生時から存在する、というか元になったものの一つであるSpice Lispの系統が未だに一番人気があるというのも面白い。


HTML generated by 3bmd in LispWorks 7.0.0

Common Lispのお宅拝見: Kyoto Common Lisp篇

Posted 2017-05-30 12:55:12 GMT

今回は、Kyoto Common Lispのcl-userを眺める。

MACLISP系方言をまとめようというのがCommon Lispの発端であったが、それ故MACLISP系方言に親しんだ人達には暗黙の前提があり作られた処理系にはそれが反映されていた。
Kyoto Common Lisp(KCL)は、新規に開発されたため、そのようなCommon Lisp仕様の暗黙の前提を洗い出し、より堅実な仕様を作ることに貢献したとされている。

そんなKCLだが、KCLからは沢山の支流があり、現在も活発なものの代表例としては、Embeddable Common Lisp(ECL)と、GNU Common Lisp(GCL)位だろうか。
GCLは、KCLからAKCLとなり、そこからGNUに渡った系譜で、ANSI CL化はされないままMaxima等の基盤として現在でも利用されているが、gmpを取り込んだりして開発体勢は死んでいないらしい。
また、ECLは、ANSI CL化された系統でUnicode化もされている。ManKai CL(MKCL)はECLからのフォークでECLに対して独自の味付けをしている、という所だろうか。

cl-userパッケージの構成

さて、cl-userの構成だが、KCLはCLtL1なので、userの構成の述べる。
userの構成は非常にシンプルで、lispをuseしているだけというもの。

ECLはANSI CL化されているが、こちらのcl-usercommon-lispをuseしているだけ。

MKCLは少し独自色があり、mkclパッケージをuseしていて、str+等独自なユーティリティが定義してある。

(str+ "foo" "bar" "baz")
→"foobarbaz"

(make-sequence 'octets 10)
→ #(0 0 0 0 0 0 0 0 0 0)

(type-of (make-sequence 'octets 10))(vector natural8 10)

GCLは、defpackageパッケージをuseしているが、CLtL1にはdefpackageがなかったので、別途defpackageが定義されたパッケージをuseしている。若干ANSI CL化されているとも言えるだろう。

さて毎度確認しているtruefalseだが、KCL系には実装されていなかった。ちょっと残念。

結び

KCL系は何の味付けもないcl-userという結果だったが、KCLらしいといえば、そうなのかもしれない。


HTML generated by 3bmd in LispWorks 7.0.0

Common Lispのお宅拝見: Clozure Common Lisp篇

Posted 2017-05-17 15:54:53 GMT

今回は、Clozure Common Lispのcl-userを眺める。
Clozure CLの系統の歴代の処理系を眺めてみたが、どうやら、cl-userの構成は、Macintosh Common Lisp 2.0(1991)で大まかな所が決まったようだ。

Clozure CLの系統の大まかな流れとしては、1987年にCoralがCoral Common Lispを発売し、間も無くFranzと共同で販売することになり年内に、Macintosh Allegro Common Lisp(MACL)となる。

1991年にMCL 2.0となるが、この頃の販売元はAppleで、言語仕様は、この頃出版されたCLtL2を追い掛けたものとなっている。

CLtL1の仕様では、基本パッケージとして、lispusersystemが必須だったが、ANSでは、common-lispcommon-lisp-userとなり、systemは必須ではなくなった。
名前が変更になった理由は、CLtL1仕様とそれ以降の仕様を一つのイメージに同居させるため等々だったようだが、実際には、lispパッケージをclパッケージという名前にしてしまうことが多かったようだ(Allegro CL、LispWorks等)。

しかし、MCLでは、このLISP-PACKAGE-NAME:COMMON-LISPにきっちり対応したようで、MCL 2.0でばっさりとlispuserを廃止して新しい名前にし、旧パッケージは、(require 'lisp-package)で読み込むようになっている。
(なお、これは現在のClozure CLでも同じ。)

関数の仕様の変更もしっかり反映しているので、

(cl:functionp 'list)
→ nil

(lisp:funcionp 'list) → t

のようにLISP-PACKAGE-NAME:COMMON-LISPで検討されていたことが、そのまま実現できている。

このように対応した処理系は、MCLの他にSymbolics CLがあるが、現在CLtL1時代のコードを動かそうとすると、きっちり分かれていた方が可搬性が高いようだ。

当時は移植性を考えてずるずるとlispclと移行した処理系が多かったのだと思うが、結局、前後の仕様が混ざる結果となり、移植性が損なわれることになったように思える。

ちなみに、1.0系統で利用されていたDresher氏が設計したオブジェクトシステムのObject LISPは削除されMCL 2.0からCLOSが搭載されることになった。

cl-userパッケージの構成

さて、CCL系統がdefpackageした時にデフォルトでuseされるパッケージは、clccl

(defpackage :foo)
→ #<Package "FOO">

(package-use-list :foo)(#<Package "CCL"> #<Package "COMMON-LISP">)

cclパッケージは、もともとcoral clの略なのだと思うが、MCL時代もそのまま使われ続け、さらに、Open MCLが処理系名を変更する際には、cclを活かしてClozure CLとしたので原点回帰した。

そのcclパッケージだが、700〜1000を越えるシンボルがエクスポートされている。
古くからある他の処理系と同じく、かなりのごった煮パッケージだが、ユーティリティや処理系拡張が占めている。
さらにMCL時代は、FREDというEmacsが同梱されていて、これがcclパッケージにいるので、これが大分大きくしているようだ。

MCL 2.0位の時期は、処理系拡張はとにかくcclパッケージに入れるという感じだったようだが、Clozure CLでは、多少分別されるようになったらしい。

また、今回も恒例のユーティリティに定義されていることが多いtruefalseの調査を実施。
確認できる限りでも、Macintosh Allegro Common Lisp 1.2.2(1989)時代からCCLパッケージに存在するらしい。

(mapcar #'true lambda-list-keywords)(t t t t t t t t)
(mapcar #'false lambda-list-keywords)(nil nil nil nil nil nil nil nil)

結び

現在のClozure Common Lispの源流であるCoral Common Lispが登場してから30周年らしい。
Spice LispがIBM RT PC上のMachに移植されCMU Common Lispとなったのが1987年、Allegro CLの最初の実装(TEK Common Lisp)が1986年、最初のCLISPが登場したのが1987年、のようだが、現在も生き残っている処理系が続々と30周年を迎えている。
そもそも生き残っているというのが凄いが。


HTML generated by 3bmd in LispWorks 7.0.0

Common Lispでローカル定数の構文

Posted 2017-05-15 16:31:37 GMT

C#にもローカル定数の構文が導入されるとのことだが、Common Lispにも欲しいという声を目にしたので、ちょっと試しに作ってみた。

(defmacro const (var)
  `((lambda () ,var)))

(defmacro ket ((&rest binds) &body body) (loop :for (var val) :in binds :for gvar := (gensym (string var)) :collect `(,gvar ,val) :into gs :collect `(,var (const ,gvar)) :into cs :finally (return `(let (,@gs) (symbol-macrolet (,@cs) ,@body)))))

const構文にあまり意味はなく、直接lambdaを書いてしまっても良いが、気分的に定義してみた。

(defun fib (n)
  (declare (optimize (speed 3) (safety 0) (debug 0) (hcl:fixnum-safety 0))
           (type fixnum n))
  (ket ((n n))
    (if (< n 2)
        n
        (+ (fib (1- n))
           (fib (- n 2))))))

こんな感じに書いてもコンパイラが最適化してくれるので、ketは無かったことになることが多いだろう(少なくともLispWorksではそうなる)

(defun fib (n)
  (declare (optimize (speed 3) (safety 0) (debug 0) (hcl:fixnum-safety 0))
           (type fixnum n))
  (ket ((n n))
    (if (< n 2)
        n
        (+ (fib (decf n))
           (fib (decf n))))))

こういうのはマクロ展開時にエラーになる。

このketは、定数を宣言しているのではなく、setfsetqでエラーを起すようにしたもの。

(let ((x 42))
  (setf (const x) 42))

でエラーになるようにしたと考えれば判り易いだろう。
それ故、setfsetq時のエラー内容は全く定数云々の件とは異なるので、この辺りに手抜き感が漂う。

defconstantを使うものに展開するという手もあるが、defconstantはトップレベルに置かないと上手く機能せず、そこをコードウォークでやりくりするにしても色々面倒なので、まあこの辺りで手を打ってみた。

ちなみに、ローカルな定数構文の提案は、Common Lispの仕様策定時にはあり、1987年にlet-constant/constantletという代物が提案されている。
(declare (constant foo))のような宣言も提案されていたようだが、しかし、紆余曲折でどこかに行ってしまったようだ。

結び

やっぱりコンパイラで対応してくれないときびしい。


HTML generated by 3bmd in LispWorks 7.0.0

Common Lispのお宅拝見: Xerox Common Lisp篇

Posted 2017-05-09 21:09:29 GMT

今回は、Xerox Common Lisp(Medley3.5)のxcl-userを眺める。
Medleyは、XeroxのLispマシンであるInterlisp-Dマシンの仮想マシン版。
Interlisp-DマシンもCommon Lispの普及に応じて取り込んだため、Common Lispも使えるのだった。
Interlisp-Dで実装されているため、マクロ展開などではInterlisp-Dの関数等が見えてたりして中々面白い。

2002年の時点では、$400から$2000位で各種環境が販売されていたようだが、現在ではどうなのだろうか。

Medleyの導入については過去に幾つか書いているので興味のある方は参照されたい。

さて、Medleyは、ANSI CL化以前という所なので、ユーザーのホームパッケージは、cl-userではなく、userである。
しかし、Xerox CL(XCL)では、xcl-userというのを用意してこちらをデフォルトにしているので、今回は、こちらを紹介することにする。
ちなみに、userは、lispをuseしているだけのパッケージとして用意されてはいる。

(package-use-list :xcl-user)
→ (#<Package LISP> #<Package XEROX-COMMON-LISP>) 

となっている。

xerox-common-lisp(xcl)パッケージは、大体180位のシンボルで、大抵の処理系と同じく、マルチプロセス等の拡張機能、ユーティリティで占められている。
ANSI CL規格以前にはdefpackageは無いが、xclパッケージには用意されているので試してみると、

(defpackage :foo)
→ #<Package FOO>

(package-use-list :foo)(#<Package LISP>)

となり、lispパッケージがuseされるのみ。

ざっと眺めて面白そうなユーティリティを紹介してみると、

XCL:WITH-COLLECTION

SBCLなどにもあるリスト集積のユーティリティ

(with-collection
  (collect 'x)
  (collect 'x))(x x)

XCL:DESTRUCTURING-SETQ

destructuring-bindもCLtL1には存在しなかったが、xcl:destructuring-bindと一緒にsetq版も定義されている。

(let (a d)
  (destructuring-setq ((a . d)) '((1 . 2)))
  (list a d))(1 2)

また、今回も非常にどうでも良い所ではあるが、ユーティリティに定義されていることが多いtruefalseを調べてみた。
XCLには存在したが、もしかしたらCLtL1な処理系でお馴染だったのかもしれない。

(mapcar #'true (il:for i il:from 1 il:to 20 il:collect i))(t t t t t t t t t t t t t t t t t t t t) 
(mapcar #'false (il:for i il:from 1 il:to 10 il:collect i))(nil nil nil nil nil nil nil nil nil nil) 

結び

そういえば、Interlisp-Dといえば、LOOPSなのだが体験できる環境がなく試せていない。
実機のディスクイメージは多数公開されているので、Medley以外でエミュレータが実現されればもしや使えるかも……。


HTML generated by 3bmd in LispWorks 7.0.0

Common Lispのお宅拝見: Allegro Common Lisp篇

Posted 2017-05-09 06:00:20 GMT

今回は、Allegro Common Lisp 10.1のcl-userを眺める。
Allegro CLもLispWorksと同様に様々なプラットフォームで稼動しIDEも付属するが、LispWorksと違ってGUI環境のパッケージは別になっていて、CUIの時は通常読み込まれない。
とりあえず、cl-userの中身を確認してみると、

(package-use-list :cl-user)
→ (#<The COMMON-LISP package> #<The EXCL package>) 

となっている。
GUI環境では、cl-userではなく、cg-userがホームパッケージとなり、cgパッケージがこれに加わる。

このexclパッケージがかなり大きく、シンボル数735、そのうち関数513、変数53というごった煮ユーティリティパッケージとなっている。

defpackage時でオプションを省略した場合のデフォルトでは、clしかuseされず、割合に素直な印象。
とはいえ、結局処理系ごとにまちまちなので、(:use :cl)と明示的に書くことになるのだが。

(defpackage :foo)
→ #<The FOO package>

(package-use-list :foo)(#<The COMMON-LISP package>)

ちなみに、exclいうのはExtended Common Lispの略で、Allegro CLの元の名前である。
Allegro CLは、Tektronix 44xxシリーズで稼動するCommon Lisp処理系(TEK CL)として誕生したが、Franzの内部的には、Extended CLとしていたらしい。

その後、Macintoshで稼動するCoral CLをFranzが共同で販売した際に、Macintosh Allegro Common Lispとし、その後、既存のExCLもAllegro CLと名称が変更になった。

閑話休題。Allegro CLのcl-userパッケージの特徴だが、正規表現ライブラリがデフォルトの状態で使えたりすることだろうか。

(re-let "(.*)\\s+(.*)\\s+(.*)" "foo bar baz"
        ((x 3) (y 1) (z 2))
  (list x y z))

("baz" "foo" "bar")

正規表現がデフォルトのcl-userで使える処理系というのは案外少ない。筆者が知る限りというか唯一かもしれない(CLISPもそうかと思ったがregexpパッケージはuseされていないようだ)。

その他は、LispWorksと同じくCLtL1時代からのユーティリティが多数定義してある。
ここ数年マルチスレッド対応が徹底してきた為、exclパッケージはさらに肥大したようだ。

また、今回も非常にどうでも良い所ではあるが、ユーティリティに定義されていることが多いtruefalseを調べてみたが、意外にもエクスポートされていなかった。
筆者が定番と思っていただけだったかもしれない……。

(mapcar #'excl::true (loop :repeat 20 :for i :from 1 :collect i))(t t t t t t t t t t t t t t t t t t t t) 
(mapcar #'excl::false (loop :repeat 20 :for i :from 1 :collect i))(nil nil nil nil nil nil nil nil nil nil) 

結び

Allegro CLの‘excl’のように処理系独自のユーティリティパッケージは非常に面白く、詳細に眺めてみたい所なのだが膨大できりがない。
なんとかコンパクトにまとめられると良いのだが……。


HTML generated by 3bmd in LispWorks 7.0.0

Common Lispのお宅拝見: LispWorks篇

Posted 2017-05-07 15:23:16 GMT

Common Lispを利用する上で普段何気無く利用しているcl-userだが、実は処理系毎にユーティリティの充実度等が結構違っている。
そこで、ホームパッケージであるcl-userパッケージを処理系毎に観察してみよう。

今回は、LispWorks 7.0 を眺める。
LispWorksは様々なプラットフォームで稼動するのでプラットフォーム依存な所はあるのだが、cl-userのが取り込んでいるパッケージは下記の通りで、LISPWORKS(lw)とHARLEQUIN-COMMON-LISP(hcl)を取り込んでいる。

(package-use-list :cl-user)

→ (#<The COMMON-LISP package, 2/4 internal, 978/1024 external> #<The HARLEQUIN-COMMON-LISP package, 0/8 internal, 353/512 external> #<The LISPWORKS package, 0/4 internal, 224/256 external>)

defpackageした際に:useを省略するとデフォルトのパッケージが取り込まれるが、上記3つが:useされるのがLispWorksの標準。

ちなみに、Harlequin Common LispというのはLispWorksを元々作っていた会社がHarlequinということに由来する。
Harlequin社は、〜Worksという名前で製品を作ることが多かったが、元々は、Harlequin CLを中心とした開発環境がLispWorksということだったらしい。

lwパッケージとhclパッケージの使い分けが判然としないが、hclパッケージの方がCLtL1時代からのユーティリティが多い気がするが、lwhclを合せて600近くのシンボルがあるので、結構ごちゃごちゃしているなあという印象はある。

これらユーティリティで有名なところでは、when-letif-letwith-unique-namesrebindingsplit-sequence辺りだろうか。
with-unique-namesは、with-gensymrebindingonce-onlyとして知られているマクロだが結構見掛けることは多いかと思う。
split-sequenceはQuicklispにもあるユーティリティと同名だが、LispWorksがオリジナルなのかもしれない(とはいえ微妙に仕様が違う)

また、オリジナルのloopにあったユーザー定義の構文がデフォルトで使えるので、

(define-loop-macro for)
(define-loop-macro repeat)
(define-loop-macro with)

(for i :from 0 :repeat 10 :collect i) ;=> (0 1 2 3 4 5 6 7 8 9)

こんなこともできるし、さらにユーザーがデータ型の処理方法を任意に定義することもできる(defloop等)

また、非常にどうでも良い所ではあるが、ユーティリティに定義されていることが多いtruefalseも用意されている。

(mapcar #'true (repeat 20 :for i :from 1 :collect i))
;=> (t t t t t t t t t t t t t t t t t t t t) 
(mapcar #'false (repeat 10 :for i :from 1 :collect i))
;=> (nil nil nil nil nil nil nil nil nil nil) 

Common Lispにはconstantlyがあるので不要に思えるのだが、用意している処理系は結構ある。

結び

以前から、処理系ごとにcl-userを比較してみていたが、これまで資料は貯めていたものの何故か書いたことがなかった。
今後、暇潰しに他の処理系についても書いてみるつもりである。


HTML generated by 3bmd in LispWorks 7.0.0

Lisp本積読解消: 素数夜曲―女王陛下のLISP: B.5.3 構文の拡張

Posted 2017-05-07 07:04:39 GMT

今回は、素数夜曲―女王陛下のLISPのマクロの箇所を読む。
本書は、数学の本なのだが、Lisp(Scheme)についての内容が半分を越えるということで知られているらしい。
マクロの解説もあったので眺めてみた。

B.5.3 構文の拡張

著者は、例外をできるだけ少なくなるようにプリミティブまで分解した結果、利用者がプリミティブを組み合せて目的の物を作るというScheme流のスタイルが生れてくるという解釈をしている。
関数と残りの例外である特殊形式まで分解した結果、それらを組み合せるというマクロが生きてくる、という解釈。
綺麗な解釈だとは思うが、正直考え過ぎかなと思った。
ただ、Lispマクロは特殊形式を減らそうというのが出自であったので、著者の考えと順番は逆にはなるが、プリミティブに分解する方向では同じかもしれない。

さて、マクロの解説だが、構文の拡張の例として、ifthenelseのキーワードをつける例を紹介。syntax-rulesを用いるので特にややこしい説明はない。
次に、notandが合体したnandincdecを作る。

最後に、delayed consの構文を定義するということで、s-conss-cars-cdrを定義し、delayforceを隠蔽してみせる。
関数でも定義可能なものになっているので、何故マクロを使ったのかは分からないが、この章は構文の拡張がお題だからだろう。

結び

数学の本なのに一応Schemeマクロの解説までしてあるというのは凄いと思う。


HTML generated by 3bmd in LispWorks 7.0.0

CL-Cleanup、CL-Compilerメーリングリストをまとめてみた

Posted 2017-05-04 07:45:45 GMT

なんとなくsaildart.orgからCL-Cleanup、CL-Compilerメーリングリストのファイルを抜き出して纏めてみた。
メーリングリストの期間や、元データ等の詳細は、ml.cddddr.orgのフロントページに記載してある。

CL-Cleanupの方は、HyperSpecの付録として配布されているので割合に目にしたことがあるかもしれない。
あのイシューを議論していたのが、CL-Cleanupだったらしい。

CL-Cleanupを眺めて興味を持ったらHyperSpecのまとめを参照してみるというのも良いかもしれない。

どうも:1をどういう風に解釈したら良いかも議論して決めたことだったらしく感心してしまう。

CL-Compilerの方も眺めてみると興味深いことが多いというか、Common Lispの理解が深まりそうな気がする。


HTML generated by 3bmd in LispWorks 7.0.0

Common Lispのeltとnthの違い

Posted 2017-05-02 04:41:51 GMT

Common Lispのeltsequence全般に使えるが、リストで使う場合には、nthとは微妙に挙動が違う。

(let ((u '(0 1 2 3 4 5 6 7 8 9) ))
  (list (nth 100 u)
        (multiple-value-list (ignore-errors (elt u 100)))))
;=> (nil (nil #<conditions:index-out-of-range 40202E27E3>)) 

eltは上記のようにシークエンスの範囲外ではエラーとなるが、インデックスはvalid array indexであることと定められているのであった。

対してnthはインデックスは、a non-negative integerである。 範囲外ではnilを返す。

ちなみに似たようなものには、endpnullの関係がある。

(let ((u '(a . d)))
  (list (null (cdr u))
        (multiple-value-list (ignore-errors (endp (cdr u))))))
;=> (nil (nil #<type-error 402015AD53>)) 

結び

どこかで書いたことがある気がしたが、どこに書いたのか思い出せないのでまた書いてしまった。
チェックも厳しいのでeltで済むなら、elt書いた方が良いだろう。
ちなみに、eltで書いても最適化すれば大抵の場合nth同等になる。


HTML generated by 3bmd in LispWorks 7.0.0

Seriesの色々

Posted 2017-04-27 16:36:10 GMT

久々にSeriesのブログ記事を見掛けたが、ブログ中にこんな疑問があった

``上のコード、実行するたびに以下のようなwarningが出るんですよね。なんとかならないものか。''

これは大抵の場合、seriesのパイプラインに載ってないのが理由である。
Seriesでは最適なコードを出すには色々な制約があり、色々繁雑な作法があるのだった。

この流れて行くと元のコードをSeries的に最適化して、やったね!という締めになりそうだが、このコードを最適化するのはどうも大変っぽいので別の書き方をすることにした。

とりあえずは、パッケージの定義から

(defpackage :Z
  (:use :cl :series))

(in-package :Z)

(series::install :implicit-map T)

まず、Seriesを最適化するにはseries::installの実行が大切である。
リーダーマクロが導入されるに留まらず、letdefunfuncallが改造されてしまう。この辺りは好き嫌いが分かれるだろう。ちなみに筆者はあまり好きではない。

implicit-mapはデフォルトではnilだが筆者はこの怪しい機能が好きなのでTにしたい。

implicit-mapTだとこのように書ける

(let ((e (scan-range))
      (s (scan "響け!ユーフォニアム")))
  (collect (list e s)))
;=> ((0 #\響)
;   (1 #\け)
;   (2 #\!)
;   (3 #\ユ)
;   (4 #\ー)
;   (5 #\フ)
;   (6 #\ォ)
;   (7 #\ニ)
;   (8 #\ア)
;   (9 #\ム)) 

ぱっと見普通だが良く考えると色々変なことになっている。

恐らくこの機能は、seriesの前身のLetSの構文を実現するものなのではないかなと考えているが実際はどうなのだろう。

閑話休題。それで、とりあえず、clojureのcycleみたいなユーティリティを考えてみる。

(defun cycle (seq)
  (declare (optimizable-series-function 1)) 
  (let ((len (length seq)))
    (scan-fn '(values character fixnum) 
             (lambda () (values (elt seq 0) 0))
             (lambda (c prv &aux (cur (1+ prv))) 
               (declare (ignore c))
               (values (elt seq (mod cur len)) cur))
             (constantly nil))))

(subseries (cycle "響け!ユーフォニアム") 0 20) ;=> #Z(#\響 #\け #\! #\ユ #\ー #\フ #\ォ #\ニ #\ア #\ム #\響 #\け #\! #\ユ #\ー #\フ #\ォ #\ニ #\ア #\ム)

(declare (optimizable-series-function 1))が肝だが、defunもSeriesの独自定義のものに差し換えられており、後々ループに組み直す為に定義時に中身が分解されて格納されたりしている。
defunマクロを展開すると通常のcl:defunとは似ても似つかない内容になっているので眺めてみよう。

それで、本体だが、一文字ずつずらしながら改行されているということは、該当の場所の文字を改行に置き換えてしまえば良いのではないかということで、このように書いた

(defun 響け!ユーフォニアム ()
  (let* ((str "響け!ユーフォニアム")
         (len (length str)))
    (collect-ignore
     (#2M(lambda (c i)
           (write-char (if (= len (mod i (1+ len))) #\Newline c)))
         (cycle str)
         (scan-range :length (* len (+ 2 len)))))))

なお、implicit-mapが有効なので、#2Mは取ってしまえるので、こうなる

(defun 響け!ユーフォニアム ()
  (let* ((str "響け!ユーフォニアム")
         (len (length str)))
    (collect-ignore
     ((lambda (c i)
        (write-char (if (= len (mod i (1+ len))) #\Newline c)))
      (cycle str)
      (scan-range :length (* len (+ 2 len)))))))

さらにlambdaletに纏めることが可能。

(defun 響け!ユーフォニアム ()
  (let* ((str "響け!ユーフォニアム")
         (len (length str))
         (c (cycle str))
         (i (scan-range :length (* len (+ 2 len)))))
    (collect-ignore
     (write-char (if (= len (mod i (1+ len))) #\Newline c)))))

(響け!ユーフォニアム) ;>> 響け!ユーフォニアム ;>> け!ユーフォニアム響 ;>> !ユーフォニアム響け ;>> ユーフォニアム響け! ;>> ーフォニアム響け!ユ ;>> フォニアム響け!ユー ;>> ォニアム響け!ユーフ ;>> ニアム響け!ユーフォ ;>> アム響け!ユーフォニ ;>> ム響け!ユーフォニア ;>> 響け!ユーフォニアム ;=> nil

そして、この定義をマクロ展開すると、こんな恐しいことになっている (実行可能なように#:は取ってある)

(defun 響け!ユーフォニアム ()
  (cl:let* ((str "響け!ユーフォニアム"))
    (cl:let (len)
      (setq len (length str))
      (cl:let (out-137857)
        (setq out-137857 (* len (+ 2 len)))
        (cl:let (out-137847)
          (setq out-137847 (length str))
          (cl:let (out-137846 out-137845)
            (setq out-137846
                  #'(lambda (c prv &aux (cur (1+ prv)))
                      (declare (ignore c))
                      (values (elt str (mod cur out-137847)) cur)))
            (setq out-137845 (constantly nil))
            (cl:let (state-137844
                     (state-137843 0)
                     items-137848
                     (items-137842 0)
                     (i (coerce (- 0 1) 'number))
                     (counter-137854 out-137857))
              (declare (type fixnum state-137843)
                       (type fixnum items-137842)
                       (type number i)
                       (type fixnum counter-137854))
              (locally
                (declare (type character state-137844)
                         (type character items-137848))
                (values (cl:let* ()
                          (cl:multiple-value-bind (|Store-Var-137865|
                                                   |Store-Var-137866|)
                                               ((lambda () (values (elt str 0) 0)))
                            (cl:let* ()
                              (values (setq state-137844 |Store-Var-137865|)
                                      (setq state-137843 |Store-Var-137866|))))))
                (tagbody
                 ll-137864 (if (cl:funcall out-137845
                                           state-137844
                                           state-137843)
                               (go series::end)
                               nil)
                 (setq items-137848 state-137844
                       items-137842 state-137843)
                 (values (cl:let* ()
                           (cl:multiple-value-bind (|Store-Var-137867|
                                                    |Store-Var-137868|)
                                                (cl:funcall out-137846
                                                            state-137844
                                                            state-137843)
                             (cl:let* ()
                               (values (setq state-137844
                                             |Store-Var-137867|)
                                       (setq state-137843
                                             |Store-Var-137868|))))))
                 (setq i (+ i (coerce 1 'number)))
                 (if (not (plusp counter-137854)) (go series::end) nil)
                 (cl:let* ()
                   (cl:let* ()
                     (cl:let ((|Store-Var-137869|
                               (- counter-137854 1)))
                       (setq counter-137854 |Store-Var-137869|))))
                 (write-char (if (= len (mod i (1+ len)))
                                 #\Newline
                                 items-137848))
                 (go ll-137864)
                 series::end)
                nil))))))))

おわかり頂けただろうか……別々に定義した関数の中身が合体していることが分かると思う。

結び

筆者も以前はSeriesを好んで利用していたが最近は面倒なのでSeriesを使って書いたりしていない。

どうもSeriesを使って何か書いていると、Seriesが上手く最適化されないなーなどとチューニングの為に横道に逸れてしまうことが多いのだった。

Seriesだと繰り返しを関数の組み合わせのように書けるが、実際の所はまさしく黒魔術なマクロでループに式を変形していて、Seriesの作法を知らないとビシッとループには展開されない。
綺麗にループに展開されない場合は、非効率なコードが出てしまうが、冒頭の警告の話は、このような背景による。

この辺りはコンパイラがすべきことなんじゃないかなあと思ってしまうが、マクロの可能性の一つではあるのかもしれない。

とりあえず、Seriesの標準のユーティリティにあまり使い易いものはないが、Clojureあたりを参考にユーティリティを作る所から始めれば、Seriesも快適に使えたりするのかもしれない。


HTML generated by 3bmd in LispWorks 7.0.0

教養としてのCommon Lisp

Posted 2017-04-26 17:35:08 GMT

Common Lispを学ぶにも色々な動機があるが、言語オタクがCommon Lispを学ぶ上で、他の言語とはちょっと毛色が違ったアピールポイントを筆者なりに考えてみた(暇だったので)。

30年前にはかなり尖っていたかもしれない仕様だが、今となってはかなり他の言語に機能がキャッチアップされているCommon Lisp。
まず、筆者的には、対話環境がすなわち言語処理系ということがCommon Lispの特長かなと思っているのでその辺りを推したい。
evalが発展して対話環境ともなり、コンパイラとも連携しているということが醍醐味なのかなと思っている。
なお突き詰めれば、二村射影とか部分評価とかその辺りに関連していくのかもしれないが筆者は良く知らない。

それと、良く言われるProgrammable Programming Languageということ。
Lispを書くようになると当たり前に感じられることだがLispの特長だろう。
Common Lispを書けば、Programmable Programming Languageを実現するための道具立ても豊富であることも分かると思う。

などなど考えて学習トピックを並べてみた

  • 始めに対話環境があり、それがコンパイラを含むようになったという流れ

    • ランタイムにコンパイラを含め全ての機能が含まれているということ(実行時コンパイルなど)
    • インタプリタ動作とコンパイラ動作に矛盾がない仕様とその実現努力
  • Code as data、programmable programming languageということ

    • リスト処理/コード処理
    • ユーザーが目的に応じて処理系をカスタマイズするということ

      • ユーザー拡張の例:フックできる場所の例とフックを実現する機構
      • evalを直接拡張していた時代のおさらい
      • リード時、コンパイル時、ロード時評価
      • リーダーマクロ、マクロ、マクロ展開時のフック、MOP(オブジェクトシステムの動作の規約に則ったカスタマイズ)、アドバイス機構
  • 言語の策定プロセス:取捨選択のプロセス

    • 過去の処理系/Lisp文化との互換性の取捨選択
    • Lisp専用マシンから汎用マシンをメインのターゲットにしたということ
  • より発展的な処理系との比較: Racketなど (素朴 vs 先進的機能)

    • Lisp-1 vs Lisp-2
    • サブセット的な規格との比較: Common Lisp vs ISLISP (Common Lispでは何が繁雑だと考えられたか)

なお、対話環境については、コードを書いて対話的にデバッグしてみないと分からないのでコードを書くしかないかもしれない。
マクロのデバッグが困難であると先入観を持っている人は多いが、素朴な仕組みなので所詮リスト処理のデバッグが殆どである。
マクロについてはScheme/Racketしか触っていないとちょっと違った印象を持つのかもしれない。

とりあえず、Lisp族を語るならばCommon Lispを知っていることは必須だろうと思う。
何故なら多数の方言があるLispの中で良くも悪くも一つの基準となってしまったのがCommon Lispで、Schemeが反発しつつもCommon Lispの機能を取り込んだりアレンジしたりしているし、Clojureもしかり、Emacs LispはCommon Lispの前身から枝分かれしているが、rmsのCommon Lispへの反発があったり。

なんだかんだでネタ元を知るのが理解の近道だと思うし、まさに教養としてCommon Lispを押えておいた方が言語オタクの井戸端会議は盛り上がると思う。


HTML generated by 3bmd in LispWorks 7.0.0

Zetalispのlambda-macro

Posted 2017-04-23 16:30:51 GMT

今日は一日LMI Lambdaのエミュレータを触っていたが、そういえば、Zetalispにはlambda-macroというものがあることを思い出した。
(ちなみに、LMIのZetalispは、ZetaLISP-Plusらしい……)

lambda-macroの使い所だが、Common Lispでarcのfnを再現することを考えてみよう。
arcのfnは大体Common Lispのlambdaだが、fnには引数の解構destructuring機能がある。

Common Lispのマクロで書くなら、

(defmacro fn ((&rest args) &body body)
  (let ((a (gensym)))
    `(lambda (&rest ,a)
       (destructuring-bind (,@args) ,a
         ,@body))))

こんな感じで、こうなる

(mapcar (fn ((a . d)) (list a d))
        '((0 1 2 3) (1 1 2 3) (2 1 2 3)))
;=> ((0 (1 2 3)) (1 (1 2 3)) (2 (1 2 3))) 

しかしそれで元のlambdaのように

((fn ((a . d)) (list a d)) '(0 1 2 3))

と書けるかというと、こうは書けない。

ここがCommon Lispでは微妙にもどかしい所だが、Zetalispでは、lambda-macroを定義してやれば思ったように書ける。

(deflambda-macro fn ((&rest args) &body body)
  (let ((a (gensym)))
    `(lambda (&rest ,a)
       (destructuring-bind (,@args) ,a
         ,@body))))

((fn ((a . d)) (list a d)) '(0 1 2 3))
;=> (0 (1 2 3)) 

さらに、deffunctionという引数の取り方をカスタマイズした関数を定義する構文が用意されていて、上記のfnを下敷にする関数は、

(deffunction a.d fn ((a . d))
  (list a d))

(mapcar #'a.d '((0 1 2 3) (1 1 2 3) (2 1 2 3)))
;=> ((0 (1 2 3)) (1 (1 2 3)) (2 (1 2 3)))

と書ける。

結び

Lispマシンのマニュアルの解説箇所はこちら

まあ、Lisp-1 ならこういうものは素直に書けるのだが。


HTML generated by 3bmd in LispWorks 7.0.0

LMI Lambdaのエミュレータ:LambdaDelta 公開!

Posted 2017-04-23 05:55:47 GMT

最近、LispマシンのメーリングリストでLMI Lambdaのエミュレータが完成間近であることがアナウンスされていたが、今日LambdaDelta 0.98.1が公開された。
やった!、思いの外公開されるのが早かった。

早速起動したい所だが、ROMや、テープイメージ等を準備する必要がある。

必要なものを取得しつつのビルド手順は下記の通りとなった。
ホストOSは、Debian GNU/Linux 64bit

### LambdaDelta本体の準備

wget https://github.com/dseagrav/ld/releases/download/0.98.1/ld-0.98.1.tar.gz tar xvf ld-0.98.1.tar.gz cd ld-0.98.1 ./configure --without-SDL1 make

### ROMの準備

cd roms wget http://bitsavers.trailing-edge.com/pdf/lmi/firmware/SDU_15165-0003.ZIP unzip SDU_15165-0003.ZIP wget http://bitsavers.trailing-edge.com/pdf/lmi/firmware/VC_MEM.ZIP unzip VC_MEM.ZIP http://bitsavers.trailing-edge.com/pdf/lmi/firmware/Memory/2243902_OF_27S291.BIN

ln -s VC_MEM/U43_2516_2242843-0001_0856TI200038.BIN VCMEM.ROM ln -s 2243902_OF_27S291.BIN MEM.ROM ln -s SDU_15165-0003/combined.BIN SDU.ROM cd ..

### インストールテープの準備

cd tapes wget http://bitsavers.trailing-edge.com/bits/LMI/lambda/00_install.tap wget http://bitsavers.trailing-edge.com/bits/LMI/lambda/02_system.tap cd ..

### ディスクの準備

fallocate -l 1gib disks/disk.img

### 初期化ファイルの準備

echo "sdu_switch 0" > ld.conf

あとは大体READMEに書いている通りに進めばセットアップは完了する

インストール時のはまりどころ

一番最初は、ld.confをどう書くのか最初は良く分からないのでデフォルト値を利用することになる。
はまりそうなデフォルト値は、ディスクのdisks/disk.img位かなと思う。

また、イーサネットデバイスを利用するのにroot権限が必要だが、

sudo modprobe tun 
sudo tunctl -t ldtap -u ユーザー

等とするか、root権限で実行することになると思う。 Xの画面が開かない時は.Xauthorityをrootのホームディレクトリにコピーする等適当に凌げば良いかもしれない。

170423130139

起動できたら、とりあえず、ユーザーのホームディレクトリでも作成してみる。

(fs:create-directory "mc")

そしてログイン(特に何も起きないが……)

(login 'mc)

初期化ファイルは、ユーザーディレクトリのlispm.initらしい(CADRと同じ)

170423130315

バックアップテープからのリストア

F12でテープを02_system.tapに切り替えて、System B(F1を押してからB)でバックアップユーティリティを起動。 中程のメニューのModesからRETRIVEを選んで、上のCommandsメニューからRESTORE-FILESを選択すると、読み込みと展開が始まる。結構長い。

lmi-lambda-restore

結び

とりあえず速報という感じでまとめてみた。
ホストとのファイル共有や、ネットワーク周りがまだ良く分からないが、ホストとファイルをやりとりできるようになると非常に捗るので方法を探りたい。

いやー、LambdaDeltaの今後の進展が楽しみだ!


HTML generated by 3bmd in LispWorks 7.0.0

Lisp本積読解消: Notes on the Programming Language LISP: The Macro Facility

Posted 2017-04-20 15:22:38 GMT

今回は、最近bitsaversにアップされた、Bernard Greenberg氏が1976-78年に書いたLispの入門の手引きNotes on the Programming Language LISPのマクロの章を読む。

Bernerd Greenberg(BSG)氏といえば、Multics Emacsの作者として有名だが、Lispハッカーとしても有名。
そんなBSG氏がMITの学生向けに書いた手引きらしい。
書かれた時期は、BSG氏がまさにMultics Emacsを開発していた頃にあたる。

The Macro Facility

まず、Maclispで魅力的な機能の一つとしてマクロを紹介。
言語を拡張できることを説明し、具体例としては、(setq list (cons item list))のような頻出するものをユーザーレベルでpushとしてまとめられることを示す。
マクロのメリットとして、コンパイル時に展開することによる効率の高さと拡張性について触れ、個々のマクロはLispコンパイラの断片ともいえる、と述べる。 また、Lispでマクロがこれ程便利なのは、LispコードはLispデータであるということを説明し、PL/IやFORTRANではこれ程簡単に言語を拡張できないことを挙げる。
さらに、括弧で表現することに目を瞑れば、どんな拡張でもマクロ→素のLispという変換を使って書くことが可能ということと、その拡張した表現も効率よくコンパイルされうるということがマクロの最大の魅力としている。

最後にDavid Moon氏の

"Functions compute, macros translate."

という言葉を引用して締める。含蓄があるような無いような……。

結び

割と扇情的なところもあり、10年位Paul Graham氏の先取をしている所もあるなあと感じた。
全体的に面白い読み物ではないだろうか。


HTML generated by 3bmd in LispWorks 7.0.0

CADR System 78.52 其の二

Posted 2017-04-08 12:16:32 GMT

CADR System 78.52 — #:g1でams氏のプロジェクトを紹介したが、本プロジェクトは、LM-3/mit-cadrだそうで、ams氏のものは色々実験的なものだったらしい。

LM-3/mit-cadrの方を試してみたら、こちらはLinux版の場合は、これまでのようにSDLを利用するようになっていて、キーボード・マウスともに問題なく機能するようだ。

なお、usim -r ../../lispという風に明示的にシステムルートを指定する必要があるのは、現状こちらも同じらしい


HTML generated by 3bmd in LispWorks 7.0.0

Daniel Bobrow 氏と Lisp-2

Posted 2017-04-05 15:24:36 GMT

Daniel Bobrow 氏が先日3月20日に亡くなった。

氏はLISP創世記より活躍され、BBN-LISP・INTERLISPと対話型システムに於いて多大な功績を残し、Common LispにもLOOPSから始まるXerox系オブジェクト指向システムの導入に於て多大な影響を与えた人物であった。

私がDaniel Bobrow 氏で思い出すのは、氏がCommon Lispでお馴染のLisp-2の創始者ということで、昔にこのブログで書いたことがあった。
話によれば、Lisp-2は1965年あたりにBBN-LISPに実装されたのが始まりらしい。

上の記事ではnet.lang.lispを引用しているが、元の議論はMITのlisp-forumメーリングリストで行なわれていたことだったようだ。

Common Lispができつつある1983年の時点でLisp-2は古いデザインという雰囲気だが、2017年の今でも使われているというのは感慨深い。

小ネタをもう一つ、ClassicCmpのメーリングリストのBobrow氏を偲ぶスレッドで、INTERLISPの表紙について語られていた

IIRC, sometime during the project, BBNLISP was renamed INTERLISP.  I still
have the wonderful manual, with the great artwork on the cover.  Warren
Teitelman (the author) doesn't have his name on the cover.  But, the bottom
portion has a guy is operating a meat grinder, with the input being the
letters of "reference manual" in random order, and the output being
"reference manual".  Danny explained that Warren Teitelman hadn't gotten
the joke :)

Danny was funny, quick witted, friendly ... RIP.

interlisp-meat-grinder

マニュアルをひっくりかえして表紙の絵の作者を探したが、どこにも載っていなかった。作者はBobrow氏だったのだろうか。

混沌としたものをミンチにしたものがマニュアルというジョークは、CLtL2の索引のジョークを思い出す。

ちなみに、GLSとBobrow氏というと、Bobrow Stacksともいわれる Spaghetti stacksに対して、GLSは、Macaroni is better than spaghettiなんてものも書いているらしい。


HTML generated by 3bmd in LispWorks 7.0.0

CADR System 78.52

Posted 2017-04-02 07:06:34 GMT

CADRのエミュレータというとBrad Parker氏のものが有名だが、それを下敷にして色々整理したものをみつけた。

まとめているのはMIT Lispマシン(LispM)界隈では良く見掛けるams氏。

利用方法

GitHubからcloneしてmakeするだけ

git clone git@github.com:ams/mit-cadr.git

cd mit-cadr/emulator/usim

make

そして、起動
※なお、disk.imgに誤ってか意図してかコロンが末尾に付くのでdisk.imgリネームする。(そのうち直ると思われる)

./usim

自動でホストと通信してファイルも読み込んでくれるらしい。
色々試してみたが、現状では、

./usim -r ../../lisp

とLispMのルートディレクトリを明示した方が良いらしい。 (M-.でのソースコードジャンプ等で間違いがない)

(login 'foo)

とすると、ユーザーfooのホームディレクトリの初期化ファイル:lispm.initを読み込むので、ここに初期化設定を書く。
さらに、現状でソースコードジャンプを上手く機能するようにするには、初期化ファイルに、

(si:set-sys-host "server" ':unix 0404 "...//mit-cadr//lisp//")

"...//mit-cadr//lisp//"はファイルのパス

と明示すると良いようだ(なお、伝統的なMACLISPでは、/がエスケープ文字なので二重に記述する)

以下、Parker氏の配布物と比べて違っているところを列挙

  • ディスクはホストのものを利用するらしい
  • ホストのユーザーと上手く連系する
  • 時刻設定がいらない
  • chaosdserverを実行しなくてもホストのファイルが読み書きできる

なお、現状、SDLを使わず、X11を使うようにしているので、マウスやキーボード操作がLinuxでは若干上手く行ってないように思われる

また、Parker氏の配布物は、System 78.48だが、こちらでは、System 78.52と若干新しい。
記録によれば、System 78は、大体1981年末から1982年あたりのシステムで、Chinualでいうと4th Editionあたりの仕様になる。

cadr78.52

結び

MIT CADRは、最終版のSystem 99まであり、これがCommon Lisp対応となっているので、これが使えるようになると良いなあと以前から思っている。
現状、ディスクイメージはあるようなのだが、マイクロコード等細かいものが揃っていないようだ。

今後もMIT CADRエミュレータの新しい動向に注目していきたい。


HTML generated by 3bmd in LispWorks 7.0.0

Lisp本積読解消: プログラミングGauche — 18章 構文の拡張

Posted 2017-03-26 14:36:19 GMT

今回は、プログラミングGaucheのマクロの箇所を読む。
マクロについての解説は、

  • 18章 構文の拡張
  • 21.7章 マクロの調査

の二つ。後者はデバッグについて書かれている。

18章 構文の拡張

構文の拡張ということで、拡張する前の構文とは、という所から説明が始まっている。
Schemeらしくプリミティブを積み重ねていくのだという説明が続くがやや冗長な印象。

次にパターンマッチによるマクロとして、syntax-rulesマクロの解説がされる。
そして、マクロの健全性について解説され、マクロの利用の仕方について解説。

最後にsyntax-rulesでは扱いが面倒な、意図的な変数名捕捉や識別子の作成について解説し、それらを手軽に扱える伝統的なマクロ方式を解説する。
2008年の本なので、手続的に書けるマクロシステムとしては、define-macroのみだったようだ。

21.7章 マクロの調査

この章はデバッグの章なのでマクロを展開してデバッグしていく方法を解説している。
「マクロは最もデバッグの難しいプログラムです」と開始されるが、その根拠についての説明は特に無いようだ。
個人的には、Schemeの場合、緩い型なのに高階関数を何重にも積み重ねるスタイルが多用されがちなので、そちらの方が何倍もデバッグが難しいと感じるが……。

macroexpandを利用してマクロを展開しデバッグしていく方法を示す。
また、モジュールを跨いだ場合の問題点についても解説。

結び

プログラミングGaucheが出版されてから早9年だが、基本から実践的な所まで幅広く解説されているので、Schemeで何かしようと思っている人には今でもとても有用な本だと思う。
ただ、Gaucheの細かい所の仕様は結構変っているので、アップデートされたら良いなとは思う。


HTML generated by 3bmd in LispWorks 7.0.0

Lisp本積読解消: Common Lispプログラミング — 9.3 マクロ

Posted 2017-03-22 21:27:54 GMT

今回は、Common Lispプログラミングのマクロの箇所を読む。
著者のRodney A. Brooks氏は、今ではiRobotの設立者として有名。
サブサンプション・アーキテクチャでも有名で、Lisp界隈でもLucid社の設立者でありコンパイラ作者でもあった。

原書の出版は、1985年でCLtL1時代の本。
前年に発表されたCommon Lispの入門書としては最初期のものなので、他の書籍から引用されることも多かったようだ。

9.3 マクロ

最初にマクロの役割をざっと説明し、順に使い方各種を解説していく。
最初は、コードを分かり易くするためのもので、リストのアクセサとしてcadrcaddrのようなものを使っているものをfoo-namefoo-color等、プログラムの文脈的に意味のあるものにするのにマクロを使う。

次にletや、if等、制御構文系のマクロの作成方法について解説し、よりテンプレート的に書き易い道具としてバッククォートを紹介する。

そして、マクロの意図しない変数捕捉や多重評価の問題について解説してあり、マクロについて一通りの解説はされている。

気になった所

gensymの説明がちょっと間違っていたりするが誤植かもしれない(0引数と説明されているところ)

結び

お題が小分けになっているが、それぞれの説明の後には、練習問題が付いていて、なかなか良い構成だと思った。
章末には大き目の問題が付いている。
練習問題の作りも丁寧。


HTML generated by 3bmd in LispWorks 7.0.0

Lisp本積読解消: Lispプログラミング入門 — 11・4 マクロ展開

Posted 2017-03-20 13:06:24 GMT

今回は、Lispプログラミング入門のマクロの箇所を読む。
同名の書籍があるが、白川洋充著の本で、1989年出版。
対象のLisp方言はCommon Lispで、CLtL1に拠っている。ちなみに用いられる処理系は、Macintosh Allegro Common Lisp 1.2.2とのこと。
ifのインデントなど、MACLっぽいなとは思う。

11・4 マクロ展開

マクロの解説は、マップ関数の章にまとめられており、大まかにはリスト処理の仲間としているらしい。
コード生成については、前の章で触れられているので、マクロ展開として焦点を絞っているらしい。

主にlambdaからletを作ることを説明して終了。
defmacro引数の分離destructuring機能とバッククォートについて軽く触れるのみ。

章末の問題では、UCI LISPのrepeatとINTERLISPのforの繰り返し構文を作成せよとなっている。
INTERLISPのforは真面目に作ると大変だが、まあ、格好だけ似せれば良いのだろう。


HTML generated by 3bmd in LispWorks 7.0.0

Lisp本積読解消: 入門LISP — 2.16マクロ

Posted 2017-03-19 15:36:37 GMT

今回は、入門LISP―対話型アプローチで学ぶのマクロの章を読む。

原書の出版は、1986年でCommon Lispも出ていると思うが、主にFranz Lispをターゲットとして解説が進む。

本書では、Lispのプログラミングスタイルを、適用型のプログラミングスタイルと、非適用型のプログラミングスタイルに分けていて、最初は、関数呼び出しとその返り値のみを使う適用型で進み、応用として、副作用を用いる非適用型を解説する流れになっているのが、なかなか面白い構成だと思う。

Franz Lispを主なターゲットとしているので、マクロの解説でも、defdefundmの中からタイプが少ないという理由からdmを選んで説明。
ちなみに、Franz Lispは色々な方言を積極的に取り込んでいるので、独自のdef、MACLISP風のdefun、Stanford LISP 1.6系のdmがデフォルトで利用可能。

マクロの説明の前にFEXPRで特殊形式を作成する方法は解説しているので、それをマクロに置き換えていくのが主な内容となっている。

章末に問題があるが、そこそこの分量がありつつ難易度も手頃なので全部解いてみると良いだろう。
ちなみに、最後の問題は、訳の機微が微妙だと思った(2.16 7)
恐らく、FEXPRでevalを明示的に呼び出していたのをマクロを使うことによってevalを使わないようにせよ、という所だと思う。


HTML generated by 3bmd in LispWorks 7.0.0

Lisp本積読解消: LISP — 8 FEXPRとMACROの定義

Posted 2017-03-17 10:11:22 GMT

今回は、LISPのマクロの箇所を読む。
本書は、WinstonのLisp本の第一版で、原書の出版は、1981年。
安く売られていることが多いが、Common Lispではなく、MACLISPなので注意。
本章の内容もCommon Lispには存在しないFEXPRについて解説がある。

ちなみにこの本は、訳文の文体が独特なことで有名。

8 FEXPRとMACROの定義

まず、FEXPRの解説から。大まかにいって不定長引数と引数を評価しない関数であることが説明される。
関数では定義できない代表的なものとして、ifをFEXPRで定義してみせる。
しかし、FEXPRが内部でevalを呼ぶときに注意しないと変数名の競合を引き起してしまうことを説明。
この問題はマクロでエレガントに解決できるとしている(マクロも似たような問題があるが……)

そしてマクロの解説。
バッククォートを使わない(使えない?)ためsubstを使って雛形を加工していく方法が紹介されている。これはこれで面白いかもしれない。
ただ間違った置換が起きないように注意する必要はあるかも。

(defun if macro (x)
  (subst (cadddr x)
         '$$else
         (subst (caddr x)
                '$$then
                (subst (cadr x)
                       '$$pred
                       '(cond ($$pred $$then)
                              ('T $$else))))))

練習問題も定番のものが多いがそこそこ面白いので全部解いてみると良いだろう。

結び

1981年の本にしては、ちょっとLISP的な内容が古いような気がするが、1977年に出版されたArtificial IntelligenceのLISPの部分を切り出したものが原書ということなので、しょうがないのかもしれない。


HTML generated by 3bmd in LispWorks 7.0.0

Lisp本積読解消: LISP 1.5 PRIMER — 19.6 MACROS

Posted 2017-03-12 06:26:54 GMT

今回は、LISP 1.5 PRIMERのマクロの箇所を読む。
出版は、1967年で丁度半世紀前の本。
出版されたLISPの入門書としては最初期のものではないだろうか。もしかしたら最初の本なのかもしれない。

邦訳も1970年にLISP入門として出版されている。

自分が知る限りLISP入門以前にLISPの本は出版されていないようなので、日本語のLISP入門書はこれが初ではないだろうか。

表紙のタイトルがS式になっていて、こういう意匠もかなり初期の試みだったのではないだろうか。
なんとなく当時から括弧が好きだった人は好きだったんだなという感を抱いてしまう。

(LISP 1.5 PRIMER
  (BY
    (CLARK WEISSMAN)))

19 LIST STRUCTURES, PROPERTY LISTS, AND MACROS

面白いのが、リスト操作とマクロが一緒にされているところ。
実際リスト操作なのだが、バッククォートが発明されてからは、マクロを書くのはテンプレート操作という印象が強くなったように思う。
Schemeのsyntax-rulesに至ってはテンプレート言語だし。

さて、まず、特殊形式を実現するのにコンパイル時に展開するマクロが使えるということが解説される。
この辺りは、Timothy Hart氏がマクロをLISP 1.5に導入した経緯を踏まえているように思うが、この本では、特殊形式の役割は可変長引数と引数を評価しないこととしていて、マクロでそれらをどう処理できるのかを解説する。

なお、マクロの定義フォームは、MACROで、

MACRO (( (name (lambda (form) ....))
         (name (lambda (form) ....))  ))

のようになる。
ちなみに、defmacroでいうと&whole引数のようにフォーム全体が渡されてくる。

課題の一つである、可変長引数の処理については、コンパイル時にplusを固定引数の下請けである固定引数の*plusの組み合わせに展開してしまう手法を説明。

LISP 1.5では、マクロでの再帰的定義はできなかったようなので、展開用の*expandという関数を定義して、これがplus*plusに再帰的に展開する。
どうも展開用の関数を定義するというのは当時メジャーなマクロ書法だったようで1960年代後半のマニュアルなどにも頻繁に紹介されている。
展開用の関数がやっていることは、リスト操作の極みなので、マクロがリスト操作の章にあるのも分かる気はする。

また、もう一つの課題である、クォートの方は、csetという大域定数を定義する関数を、cset+quotecsetqにする例を取り上げる。

章末には、マクロの問題も、6つ程あるが面白いので解いてみることをお勧めする。
defmacroとバッククォートは使わない縛りだとなお面白い。

結び

日本の1970年代後半位の書籍だとマクロについては軽く触れられているのみだが、米国では1960年代からコンパイラと合せて紹介していることが多い。
日本ではコンパイラについてはあまり注目されなかったのだろうか。その辺が少し不思議。

ちなみに、1965年より前の文献になるとマクロは、「Timothy Hartのマクロ機構」という感じで、LISPにマクロを始めて導入したHart氏の名前と共に紹介されることが多いようだ。

なお、本書籍は、PDFがSoftware Preservation Groupからダウンロード可能なので一度眺めてみてもらいたい。


HTML generated by 3bmd in LispWorks 7.0.0

Lisp本積読解消: Dylan Programming — 6.4 Macros

Posted 2017-03-10 15:14:00 GMT

今回は、Dylan Programmingのマクロの箇所を読む。

Dylan Programmingは、1996年の出版で、主な対象としている処理系は、Harlequin Dylan。

当初、Harlequin Dylanは、DylanWorksと呼ばれていたらしいが、1.0がリリースされる前に名称変更になったらしい。

Harlequin Dylanは、1998年辺りに正式リリースがされ、1999年にEnterprise Editionがリリースされた後、色々あって、Functional Objectsという会社が扱うことになり、さらに色々あり、Open DylanとしてOSS化された。

以降、現在に至るまで、Open Dylanとして開発が活発になったり停滞したりしている。

本書のPDFは、OpenDylan.orgからダウンロード可能

6.4 Macros

DylanのマクロはLispのように言語を拡張することを目的としていて、扱いも用意であることが解説される。
なお、Dylanの競合は、C++やCなので、Cのマクロとの比較が随時差し挟まれる。

構文マクロには、パタンマッチ・テンプレート方式と、構文を操作する手続き型マクロがあるが、Dylanの場合は、テンプレート方式であることが説明される。
後々、手続き型マクロもサポートする予定だったらしい。なんと、知らなかった。

簡単なパタンマッチでマクロを書く例が示され、次にConstraintsについて説明がある。
Constraintsとは、?foo:expressionのようなパタン変数の後ろの部分で、構文に型が付いているようなもの。
Common Lispでいう&restのようなものは、?var:bodyと書けたりする。

また、サブパタンを書くことも可能で、見通しよく分割できることが示される。
マクロ内で識別子を生成する方法も解説。

次に、マクロの衛生性について解説され、識別子の意図しない捕捉等が起きないことが示される。
衛生性は意図的に破ることが可能で、?=を利用すれば利用される場所の識別子が利用できる。

次に、サブパタンを利用しては書けない例として、生成した識別子がサブパタンでは利用できない例を挙げ、補助マクロを利用する方法を解説する。

結び

最近、中置言語にLisp風のマクロを導入する例が多いが、最近の言語と比べてもDylanのマクロシステムは洗練されているように思える。
Dylanには、Common Lispの開発者が多く参加しているが、構文マクロを普通に使う人達が設計したことがシステムの洗練に大きく寄与したのではないだろうか。

Common Lispを知っている人からすると、Dylanは、まるで中置のCommon Lispで、Common Lispの精神もかなり継承しているのであるが、ブレイクしてくれなかったのが悔まれる。


HTML generated by 3bmd in LispWorks 7.0.0

Lisp本積読解消: プログラマのためのCOMMON LISPガイド — 11 マクロ

Posted 2017-03-07 18:48:22 GMT

今回は、プログラマのためのCOMMON LISPガイドのマクロの章を読む。

1988年邦訳で、原書であるA Programmer's Guide to Common Lispも1987年出版で、CLtL1な時代の本。

「プログラマのための」とあるように、何かしらの言語の経験は前提にしている。

11 マクロ

まず、マクロの効用について説明があり、次に簡単な定義の方法とバッククォートの説明がある。
次に一歩進んで、マクロを書くマクロの説明があり、ネストするバッククォートの書き方(,',の使い方等)が親切に解説されている。
そして、マクロのλリストのデストラクト分配機能について解説がある。
これら一歩進んだ機能を解説する題材として簡単なdefstructを作って行くが、defstructを作るには一通りの知識が必要となるので確かに手頃な題材だと感じた。

章末の練習問題も手頃。後半は、本章で解説した自作のdefstruct:conc-nameや、:constructorを使えるように拡張する等、実際的な問題で面白く為になる。
なお、Common Lispの仕様に忠実に:constructorをサポートするとなると結構大変だが、巻末の解答を眺めるとさすがにそこまでは要求していないらしく、名前を変更する位で良いようだ。

また、:conc-nameや、:constructorサポートするにあたって構造体名の所はリストだけのサポートで良いらしい。
デストラクト分配を使えとあるので、destructuring-bindなし(CLtL1の仕様では存在しない)でどうやるのか色々考えてしまった。
まあ、下請けのマクロを定義するだけで良いのだが……。


HTML generated by 3bmd in LispWorks 7.0.0

LispのloopやformatはUNIXでいうsedやawkである

Posted 2017-03-04 18:38:29 GMT

Common Lispのloopformatは言語内DSLの代表例と考えられていて、その機能の多さからLisperの破天荒さを象徴するものと捉える向きも多い。
しかし、これらの言語内DSLは別にLispらしさの象徴でもないし、寧ろ原理主義者からは昔から嫌われているものの代表例でもある。

formatや繰り返し構文はFortranに由来するが、やりたいことを一つの関数に詰め込んでしまったため、ごちゃごちゃすることになってしまった。

loopは、INTERLISPのCLISPというユーザーフレンドリーな機構の繰り返し機構のforが先祖で、他の言語に親しんだ人のために中置で書ける繰り返し構文をLisp内で実現したものに由来する。

Common Lispのloopformatの内情を知っていれば、Unixのsed、awkのようにシェルからは独立した処理系や、その他オプションがどんどん増える各種コマンドについての歓迎と批判が、非常に良く似た構図であることに気付くと思う。

Unix方面では、原理主義として、一つのコマンドは一つのことしかせず、それをパイプで組み合せることを徹底したPlan9文化(またはdjb氏のユーティリティ群)があり、awkのようなものがさらに発達したPerlのようなものもあり、便利なら良いじゃんということでオプションが沢山付いたGNUのユーティリティがある。

Lisp方面で言えば、Plan9はScheme文化、formatloopのようなものは、sedawkのようなもので、便利なら良いじゃんというのは、大多数のCommon Lispユーザーの立ち位置、という感じになる。 Gaucheのように全体のバランスを考えつつ便利さも失なわないように整えている環境もある。

sedawkがUnixの対話環境の象徴かといえば、そんなことはなく、パイプでコマンドを組み合せていくスタイルこそがUnixの対話環境らしさだろう。
そのような基本があった上で、便利さを追求したり統一性を重視していると思う。

Lisp方面も同じで、formatloopがLispの象徴ということはないのである。

単一の関数やコマンドに詰め込むことの批判として、徹底して小さい部品を組み合せるアプローチも多数出ている。
formatや、loopであれば、コンビネータに機能を分解して、それを組み合せる等々のアプローチがあるし、Plan9ではコマンドは本当に一つのことしかしない。
面白いのがこれらのアプローチが絶対的に使い易いかというと、そうとも言い切れない所である。細かく分割した部品が他のドメインでも上手く使えたりして一石二鳥かというと、そうでもなかったりする。

CLerは割合に現実的なので、loopformatも全機能を熟知した上で使うということもなく、適度なイディオムを利用しているのみである。
また、設計に於いても極力機能をアトミックに分割しようということも無く中庸といった所だ。


HTML generated by 3bmd in LispWorks 7.0.0

Older entries (2098 remaining)