#:g1

リーダーマクロアドベントカレンダーの残りかす

Posted 2012-12-30 01:31:00 GMT

リーダーマクロアドベントカレンダーで書くには微妙だったので見送ったけど、後で再発見してしまいそうなネタが2つあったのでメモ代わりに書いておきます。

シンボルを分解するのにリーダーマクロを使ってしまう

シンボルから文字列を取り出して、分解して再度シンボルを生成、という流れが普通ですが、リーダーマクロで分解してしまおうというネタです。
下記の例では、式を一気に文字列としていますが、シンボルごとに処理した方が無難でしょう。
(defpackage :!-demo
  (:use :cl :named-readtables))

(in-package :!-demo)

(defun !-reader (stream char) (declare (ignore char)) `(not ,(read stream t nil t)))

(defreadtable :!-reader (:merge :standard) (:macro-char #\! #'!-reader))

(defmacro with-! (() &body body) (let ((*readtable* (find-readtable :!-reader))) `(progn ,@(read-from-string (write-to-string body)))))

(with-! () (let ((true t) (false nil)) (list true false !true !false))) ;=> (T NIL NIL T)

プライムが使いたいんじゃという場合

割と定番ネタかもしれませんが、クォートがシンボル名として扱いづらいのは、terminatingp => Tなマクロ文字だからなので、これをnon-terminatingp => Tに変更すれば、所謂プライムとして使えます。
TAOでは、『'』は区切り文字ではなかったらしく、foo'のような関数/変数名が普通に扱えたようです。
(set' foo bar)のような書き方をする人も今時いないと思うので、こっちの設定がデフォルトになっても良い気はします。
(defreadtable :prime 
  (:merge :standard)
  (:macro-char #\' (get-macro-character #\') T)) ; non-terminatingp => T

(in-readtable :prime)

(defun fib (n) (fib' n 1 0))

(defun fib' (n a a') (cond ((zerop n) a') ((= 1 n) a) (:else (fib' (1- n) (+ a a') a))))

(fib 100) ;=> 354224848179261915075

2012年振り返り

Posted 2012-12-30 00:43:00 GMT

毎年振り返りのまとめを書いているので、今年も書いてみます。

1月

連日SRFIをCᴏᴍᴍᴏɴ Lɪꜱᴘに移植していたようです。暇だったとしか言い様がありませんが、自分的には結構ためになりました。

2月

ブログの更新が停滞していましたが、どうもbloggerが自分のスタイルには合わず、更新が面倒臭くなったような気がします。teepeedee2に移行した現在もそんなに楽に書けるようになってないですが…

3月

SRFIでコピペスキルが向上したため、古いMIT系 Lispマシンのコードを移植し始めたり。これがやってみると結構動くので、Lispマシンのソースは自分にとってはお宝の山です。興味のある方は是非お試しを。

4月

ブログは停滞。あまりLisp的活動もしていなかったかも。とはいえ、SRFIは移植しています。

5月

5月も更新控え目。SRFI-46の衛生マクロとか動くんかいなと思っていたところ、やってみると、これがすんなり動いたり。移植によるバグはある気がします(SchemeとCᴏᴍᴍᴏɴ Lɪꜱᴘでの差異が原因となっているタイプのもの)。

6月

ブログは更新してなかったようです。

7月

ブログは更新してなかったようです。夏バテ?

8月

ブログをteepeedee2に移行しました。コードが鬼のような最適化をするので、動きは怪しいかなと思っていましたが、思ったより安定しています。3、4ヶ月無停止で普通に動いているので、結構使えるなという感じです。

IRCで一人読書会とか開始し始めました。

lisp-newsというメルマガを発行しはじめました。

10年間不動の日本lispコミュニティのハブサイトを作りたい、とか考えていました

9月

teepeedee2の設定の一段落し書くこともなくなったのでブログは割と放置

ILC2012の準備でバタバタしていました。

Successful Lisp / David B. Lamkinsのぼっち読書会をやってました。

結論としては、Successful Lispは結構良い本です。

10月

Google Common Lisp Style Guideを翻訳してみることにしました。翻訳は、githubを利用して進めていますが、現在複数人で作業していますILC2012のLispマシン展示の手伝いとして京都へ行きました。

11月

ネタもなく更新もなく

12月

怒涛のアドベントカレンダーラッシュ。ラッシュといっても自分でやると言い出したリーダーマクロアドベントカレンダーの収拾を付ける活動が殆どでした…。で、都合24記事書いたみたいです。もうちょっと書けるかなと思っていましたが、リーダーマクロアドベントカレンダーにエネルギーを吸い取られました…。

進行中/停滞中個人プロジェクト

  • Lisp Outside the Box ぼっち読書会
  • SRFIのfinalize 全部をCᴏᴍᴍᴏɴ Lɪꜱᴘに移植
    → 後は、SRFI-72位だけど…
  • 家の積読LISP本完読
    → 増える一方…
  • Symbolicsのマニュアル読破
  • JLUGサイト改善
    → もうJLUGサイトも役目を終えた感もあるかなあ…
  • オンライン勉強会復活→ 開催してみたいけどなあ…
  • L-99完遂
    → あと、20問位だったと思うけど、残りが難しいんだよなあ…
  • Getting Started in *Lisp (*Lispのチュートリアル)完遂
  • KMRCLを読むシリーズ完遂
  • CLで学ぶ「プログラミングGauche」 完遂
  • AMOPの勉強
  • CommonObjectsがANSI CLで動くようにする
    → 全然動かせないなあこれ
  • INTERLISPで遊ぶ

来年も続けるであろうこと

来年やってみたいこと

日本のLisperが英語でブログのエントリーを書くというなんらかの企画をやってみたい

ILCでも日本人はシャイだなあ、などと言われましたが、面白いことをやってる人は結構いるので、なんらかの形でブログエントリーをまとめて、海外に発信してみたいなというところです。また、日本のLisp disネタ専門の人達と同じ土俵で活動してるのもなんだか虚しいので、発展的な何かがないかな、というところでもあります。

これまでの問題として、Cᴏᴍᴍᴏɴ Lɪꜱᴘなどでは顕著ですが、海外と交流している人はいるものの、日本の方は向いてない世界人な方が多いので、別段彼らが熱心に日本に海外の状況を紹介したりしていないという状況があります。英語喋れば良いじゃん、ということなのですが、自分としては、それは良いとして、仲立ちする層が足りないんじゃないかと考えています。

Schemeの場合は、優秀な実装者が多く、海外にも進出していますし、そこそこ日本語圏にも色々紹介しているなと感じます。
Clojureは、Cᴏᴍᴍᴏɴ Lɪꜱᴘとは違う感じですが、仲立ちとしている人は少なめではないかと思います。Clojureはウェブ方面の文化にも近いので、Ruby界隈のように直接交友している感も少しはあります。

ということで、ILCのようなものに参加しなくても、英語でブログを書いていれば、それなりに交流は持てるんじゃないかなと考えています。逆説的ではありますが、日本というものにこだわって現状を考えると、英語でブログを書いて交流と知見を日本語圏にもたらす、位しかないかなあと思ったりしています。マイナー言語なので、言語人口を考えると世界を向かざるを得ないというのもありますが。

具体的な運用方法ですが、英語でエントリーを書いたら、専用のredditに投稿して貰えれば、RSSも生成できるので簡単で良いかなとか考えています。Cᴏᴍᴍᴏɴ LɪꜱᴘとScheme等を分けないと登録したくないという細かいところもあるので(planet lispとか)、jp_lispers、jp_schemers、jp_clojurianとかに分けた方が良いかもしれません。

まとめ

2012年を振り返って、来年やってみたいことを考えてみました。引きこもりもそろそろタイムリミットなので、仕事を探します…。

過去のまとめ

#:foo の引数の活用法を考えてみた

Posted 2012-12-25 23:41:00 GMT

標準では#:のリーダーマクロの引数は無視されますが、この引数を使ってアンインターンドシンボルに番号を振って識別できるようにしたら面白いかなと思って試してみました。

定義

定義はSBCLでしか動きませんが、こんな感じ
(defvar *sharp-colon-symbols*
  (make-hash-table :test 'equal))

#+sbcl (defun sharp-colon (stream sub-char numarg) (declare (ignore sub-char)) (multiple-value-bind (token escapep colon) (sb-impl::read-extended-token stream) (declare (simple-string token) (ignore escapep)) (multiple-value-bind (symtab symtab-win) (gethash token *sharp-colon-symbols*) (unless symtab-win (let ((newtab (make-hash-table))) (setf (gethash token *sharp-colon-symbols*) newtab) (setf symtab newtab))) (multiple-value-bind (sym sym-win) (gethash numarg symtab) (unless sym-win (let ((newsym (make-symbol token))) (when numarg ;; #n:foo (setf (gethash numarg symtab) newsym)) (setf sym newsym))) (cond (*read-suppress* nil) (colon (error 'simple-reader-error "The symbol following #: contains a package marker: ~S" token)) (t sym))))))

(set-dispatch-macro-character ## #: #'sharp-colon)
などで有効にします

試してみる

(let ((#1:foo 8)
      (#2:foo 9))
  (list #1:foo
        #2:foo
        '#1:foo
        '#:foo
        (eq '#1:foo '#1:foo)
        (eq '#:foo '#:foo)))
;=>  (8 9 #:FOO #:FOO T NIL)

(eq '#0:foo '#0:foo) ;=> T (defun #0:fib (n #1:a #2:a) (cond ((zerop n) #2:a) ((= 1 n) #1:a) (T (#0:fib (1- n) (+ #1:a #2:a) #1:a))))

(funcall '#0:fib 8 1 0) ;=> 21

何故かSLIMEでslime-compile-defunすると取りこぼす(ファイルに書き出す時に#:のシンボルの情報が落ちる?)ので
(eval (read-from-string
  "(compile
 (defun fib (n)
   (#0:fib n 1 0)))"))

(fib 100) ;=> 354224848179261915075

としてみる

まとめ

使えそうな、使えなそうな微妙なところです。
#:fooという関数名やマクロは、定義がどっかに飛んでいって無くなってしまうという経験が何度かありますが、なんなんでしょう。GCされるとか?

CLハマリメモ: シンボルのeq

Posted 2012-12-25 14:57:59 GMT

(defpackage "P")

(let* ((s1 (progn (delete-package 'p) (make-package 'p) (intern "S" 'p))) (s2 (progn (delete-package 'p) (make-package 'p) (intern "S" 'p)))) (list s1 s2 (eq s1 s2))) ;=> (#:S P::S NIL)

こんなんでハマった。これは滅多にない。 実際のところもっと解りづらくて、

(defun foo (sym)
  (eq 'p::s sym))

のようなものを定義してパッケージを破壊。 その後、パッケージを同名で作成してfooを使うと、p::sを与えても、eqでTにならないという

(defun foo (sym)
  (string= 'p::s sym))

だったらOK。

パッケージの破壊により

(string= '#:s sym)

となっているので、これは当然。

パッケージを最初に作っといて、競合したのでパッケージを作り直したりとかしてぐちゃぐちゃやらない限り起らない。

同名のシンボルがeqで比較できないけど、string=では比較できてる、という謎挙動の場合は、こんな状況になっているかもしれない。


HTML generated by 3bmd in LispWorks 7.0.0

リーダーの差し替え・その他色々なアイデア

Posted 2012-12-24 15:00:00 GMT

リーダーの差し替え

まず、リーダーの差し替えの差し替えとは何かを考えると、何らかのタイミングで標準のリーダーとは別のリーダーで読むこと、と考えられます。
そういう意味では、特定の範囲をリーダーマクロで文字列として解釈し、用意した任意のリーダーで、その範囲を読み取ることも差し替えと言えば差し替えです。インラインで別の構文を実現という際には、こちらのパターンかなと思います。
これとは別にファイル単位でリーダーを変更できると、まさに差し替えているという感じで、差し替えといえばこちらが本命かもしれません。
ファイルを一つの文字列と考え、これを任意のリーダーで読むという違いがあるだけですが、標準でリーダーを差し替える機構を提供しているLisp方言は、それほど多くないようです。
自分が知っている限りでは、アドベントカレンダー 5日目でも紹介された、Racketや、MIT Lispマシンでのファイルの属性リストでの構文/リーダーの切り替え(-*- readtable: foo; syntax: foo -*-)位です。
MIT Lispマシンの方式は、ファイルのロード時にリーダーを切り換える仕組みがあれば実現できるかと思います。また、Racketの場合は、特にファイル単位に限定されるものではありませんし、#lang fooというリーダーマクロ以降を文字列として指定したリーダーで読めば、できなくもありません。
何れにせよ、リーダーの差し替えになると、統一された機構と、使い勝手のバランスという感じになるかと思われます。
リーダーマクロではなく、リーダーを差し替えることのメリットとデメリットですが、
  • メリット:
    • マクロ文字の仕組みに由来する制限はなく、全くの自由
  • デメリット:
    • 全くの自由であるだけに、一から作るのが面倒
という点が挙げられるかなと思います。実際には、リーダーマクロの方式折衷したりもすると思いますが、適材適所で選択することになるかと思います。

その他色々なアイデア

ここでは、一連のリーダーマクロ解説の中で個別でエントリーにして紹介するには難しかった雑多なものを紹介したいと思います。

Common Lispの#c(1 0)

複素数の表記ですが、Common Lispでは、実部と虚部をリストで表現するリーダーマクロで実現されています。

MACLISPの#:

MACLISPでは、#:SI:FOOと書くと、FOOとパッケージ部を読み飛ばす仕組みがありました。用途は不明…

Common Lispの#:

リード時に生成する非インターンシンボルです。#:fooは、リード時に(make-symbol "FOO")されるものと考えれば、ある意味リテラル表記とも考えられます。

Common Lisp等の\・||

Common Lispでのエスケープです。MACLISPや、Zetalispでは、\ではなく、/でしたが、任意のエスケープ文字も設定すれば簡単に他の方式で読めるようになります。

評価フェーズの指定 #.・#,・#= 等

MACLISP系での#.(Clojureでは#=)や、#,ですが、#.はリード時、#,はロード時にevalを発動します。
リード時にevalが使えるということは、予めリーダーマクロを設定すること無しに何でもできるということですが、巨大で難解な物を書く位ならリーダーマクロを設定した方が良いかと思います。ちなみに、#,はANSI Common Lispでは廃止になり別の仕組みが提供されています。

MACLISPの#%

リード時のマクロ展開です。恐らくですが、昔の処理系が遅かった時代にインタプリタ動作時のマクロ展開のオーバーヘッドを避けるために利用したのではないかと思います。もしくは、コンパイル時に明示的に全部展開される訳ではない状況化で必要になったのかもしれません。

Franz Lispの中置マクロ文字

Franz Lispでは、中置マクロ文字の仕組みがあり、
(setsyntax '+ 'vinfix-macro 'infix-plus)
のように+に展開関数を設定した場合、
(3 + 4)
(+ 3 4)
として読む機能が標準で装備されていました。動作原理ですが、Franz Lispでは、リーダーがストリームを読む時に一緒にリストが作成されていて(tconsのリスト)、そのリストが結果として返されるようなのですが、中置マクロ文字が来るとそのリストを破壊的に変更して順番を入れ換える、という方法のようです。ちょっと無理矢理な気もするのですが面白いと思います。Common Lispなどでも、ストリームをカスタマイズしたりすれば、どうにか実現できたりもするのかもしれません。

cl-annot

@m2ym氏作のリーダーマクロを活用したライブラリです。@+シンボルで以降の式をどう読むかを切り替えます。後続の式を幾つで一纏めにするかも指定できます。本来の使い方とは別にリーダーマクロでの定番アイデアもそのまま活用できます。
;;; コメント
(defannotation comment (ignore) (:arity 1)
  (cl:declare (cl:ignore ignore))
  :comment)

@comment (defun ...)

;;; gaucheの#?=的なもの
(defannotation ?= (expr) (:arity 1)
  `(progn
     (format *error-output* "~&--> ~A~%" ,expr)
     ,expr))

(defun foo (n) (if (zerop n) :done (foo @?=(- n 1))))

(foo 8) ;!! --> 7 ;!! --> 6 ;!! --> 5 ;!! --> 4 ;!! --> 3 ;!! --> 2 ;!! --> 1 ;!! --> 0 ;=> :DONE

infix-dollar-reader

@ichimal氏作のリーダーマクロを活用したGaucheの$マクロ的なものです。$にリーダーマクロを設定することによって
(+ 1 2 $ * 3 4 $ + 5 6)
(+ 1 2 (* 3 4 (+ 5 6)))
という風に展開します。対応する括弧をリード中にunread-charで戻すことにより括弧の対応を取っているところが面白いかなと思います。

まとめ

リーダーマクロに相当するものをサポートする言語はそれ程多くありません。
Lispでは、あまりサポートされていないような類の機能が多めにありますが、他の言語であまり使われてない機能は、大抵『使ってはいけない機能』と見做されることが多いなと感じます。
『使ってはいけない機能』と見做されるのは何故かと考えると、あまりにも自由奔放で制御不能に陥るに違いないという先入観に由来していることも多いと感じます。しかし、実際のところLispでは、制御するための機構も一緒に用意されていて、十分に制御できることが殆どです。
『関数を乱用してはいけない』というような話はあまり聞きませんが、制御できる確度が高ければ、乱用という見方からも遠くなるのではないでしょうか。実際のところやりすぎ機能も沢山ありますが、まずは、他の言語の先入観でLispを見ないで、制御する方法をしっかり学び活用すれば良いのではないかと思います。

以上、25日間に渡ってのリーダーマクロのアドベントカレンダーでした。
最終的に参加者は、g000001・garaemonさん・tk_ripleさん・ponkoreさん・sirohukuさん・mgikenさん・Yubeshiさん・snmstsさんの8名となりました。記事を書いてもらった皆さんありがとうございました。
リーダーマクロという非常にしばりのキツいジャンルでありながら、8名も参加したということには、正直驚いています。
個人的には途中ネタ切れになり、かなりしんどい事態となりましたが、なんとか完走できて良かったです。
来年もMOPアドベントカレンダーのように、しばりのきついテーマでなにかやってみたいところです。

()の再定義

Posted 2012-12-23 17:10:00 GMT

もうネタがないので、禁断の()の再定義について自分が試したことを紹介してみたいと思います。

()を再定義したい

そもそも()を再定義したいことなんてあるのか、という話になりますが、私の経験上では一例しかありません。
その一例ですが、TAO/ELISという日本が誇る自由過ぎるLisp方言で『!』という文字が多義的に使われていたのをどうにか再現してみたい、と考えた末、()を再定義するのが一番簡単だ、という結論になり再定義してみることとなりました。
参照:

TAOの『!』の扱いについては、ブログでも以前にも何度か考察していましたが(TAOの!再び)、リーダーマクロを定義という観点から、少し詳しく解説してみたいと思います。

TAO/ELISでの『!』

まず、TAO/ELISで『!』という文字がどのように使われているかを説明すると、

  1. (!x 42) というsetfのような代入式
  2. (!!foo x y !z) という自己代入式
    • (!!foo x y !z) ≡ (setq z (foo x y z))
  3. (!!foo x y !z) という自己代入式での代入先のシンボルを示す印(ここでは!z)
  4. (! x y z) という論理プログラミングでのバックトラックするor(『!』の後に空白あり)
  5. (! x y ! z) という論理プログラミングでのカット記号
  6. (cdr! ...)・(cons! ...)・(append! ..)という関数名
となっています。
参照:
まず、(!x 42)のようなものを(setf x 42)のように展開するには安直に『!』をsetfに置き換えれば良いのですが、そうすると(! ...)が上手く行きません。
それだったら、『!』の後の一文字を見て分岐すれば良いじゃないかということになります。
これで、(!x ...)・(!!foo ...)・(! ...)はOKです。non-terminating-pをTにすれば、(cdr\!)ではなく、(cdr! ...)と書けます。
これで解決かというところなのですが、文字を展開するだけのリーダーマクロだけに、自己代入式の(!!foo x y !z)の!zと、(!z 42)の!zの区別が付かないので
(let ((x 1)(y 2) (z 3))
  (!!list (!x 100) (!y 200) (!z (list !y)))
  y)
;=>  (100 200 (200))
のようなものは、
(let ((x 1) (y 2) (z 3))
   (selfassign list
               (setf x 100)
               (setf y 200)
               (setf z (list setf y)))  ;!!!
   y)
という風に展開されてしまいます。また、論理プログラミングのカット記号も
(! x y ! z)
のようなものは、
(logic-or x y setf z)
と展開されてしまいます。
これらの根本的な原因は何かと考えると、!xが関数呼び出しの位置で呼ばれたのか、引数の位置で呼ばれたのかがリーダーには分からないということにあります。
これを読み取り時に判定させるには、『(』の読み取りで次の文字を読んで分岐させる位しか自分には思い付きませんでした。
『(』を分岐の基点にして、関数位置だけを処理することにすれば、引数の位置のものは、そのまま未処理で次に渡せるので、関数位置と引数位置の区別は可能ということになります。
TAOでは、豊富なタグを使って『(!』という種類の特殊な括弧(オブジェクト)を扱うようになっているようなので、処理方式としては、ある意味近いかなというところでもあります。

練習問題 24. 上記のようなTAO/ELISの『!』を作ってみましょう

リーダーマクロの話が一段落したので、全く話の流れとは関係がないのですが、折角TAOの『!』について触れたので、書き残しておきます。
TAOでは、当初代入や、自己代入式は、(!x 42)・(!!foo !x y z)ではなく、(:x 42)・(::foo !x z)だったようで、途中から『!』に変更になったようです。何故変更になったのかは、ELIS復活祭での竹内先生のお話でもはっきりした由来が定かではなかったようですが、Common Lispがキーワードで『:』を使ったこととの競合回避ではないかとのことです。

まとめ

以上、今回は、()の再定義について書いてみました。
()はLispにとって最も重要といっても良い文字だと思いますが、これの再定義に失敗した場合、何から何までおかしくなってしまうことが多いかと思います。
開発に於てはリードテーブルを切り換える仕組みは必須ですが、何か変だなと思ったら、潔く処理系を再起動しましょう…。

Arcの構文をCommon Lispで(通常のリーダーで foo.barをどうするか)

Posted 2012-12-21 13:35:00 GMT

ArcのcomposeをCLでがんばるの焼き直しネタなのですが、Arcの構文を無理矢理Common Lisp上に再現してみて、それなりに使えそうという感じになったので、再度紹介します。

標準のリーダーでfoo.barを(foo bar)や、(bar foo)と読ませるには

foo.bar を(foo bar)と読ませたいということは、たまにありますが、リーダーマクロを『.』に付けるにしても基本的にマクロ文字より先の物しか一纏めにできないので、foo (dot bar)というような読み方しかできません。
ちょっと妥協して、foo.barと書くところを、.foo.barと書けば、(dot foo (dot bar))のようにできるので、これで良しとするのが無難なところかと思います。
常に関数名より先にマクロ文字が来ればOKなのですが、ここで発想をちょっと転換して、常に先頭に来ているものはないかを考えてみると、関数名の先頭の文字にリーダーマクロを付けてしまえば良いのではないか、ということになります。つまり、foo.barであれば、『f』にマクロ文字が設定してあれば、(f foo (dot bar))と読むことは可能ということになります。
foo.bar => (foo bar)の実現の手順としては、次のようになります
  1. fにリーダーマクロが設定してある
  2. 読み取り時にfに遭遇するとfのリーダーマクロ起動
  3. 一文字読み進んでいるので、一文字戻して、通常のリードテーブルのreadで読み直し
  4. foo.barというシンボルを得る
  5. シンボルの文字を『.』で分割
  6. ("foo" "bar")を得る
  7. 後は好きなように変形する
もちろん、この方法では『f』だけでは実用になりません。最低でもアルファベットの範囲は設定しておく必要があるかと思うのでリードテーブルへの登録は、
(let ((*readtable* (named-readtables:find-readtable :arc)))
  (map nil 
       (lambda (c)
         (set-macro-character c 
                              #'foo.bar-reader
                              t ))
       "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ))
のようになるでしょう。
上記の例では、『.』のみですが、通常のシンボルとして読み込んだときにシンボル名から判定して、foo.bar => (foo bar)の他にもfoo:bar => (compose foo bar)のような読み取りも可能です。実際ArcでもSchemeレベルではシンボルとして読んで、名前の文字列を分解して再合成という上記と同じ方法で処理をしています。

練習問題 23. 上記のような方法で、foo.barを(foo bar)と読んだり、(get 'foo 'bar)と読んだり、させてみましょう

まとめ

以上、今回は、ドットの連結を標準のリーダーでどう処理するかの紹介をしてみました。
結構無理矢理で、プロダクションレベルで使うようなものではないと思いますが、そこそこ使えます。実際、自分は、日常的にメインのリードテーブルは上記のように設定して使ってみていますが、特にこれといったトラブルもありません。
この辺りになると、標準のリードテーブルとかなり違うので、標準のリードテーブルと混ざらないように切り換える仕組み(named-readtables)が必須になってきます。そうしないと、普通の使い方が全くできなくなったりしますので、試す際にはリードテーブルを管理するツールの利用を前提に試してみてください。
Scheme等では、処理系の独自機能で、こういう中置で連結するリーダーマクロをユーザーが簡単に定義できるような仕組みをサポートしてみる、というのも面白そうですね。

perlfilterの例をリーダーマクロで実装してみる

Posted 2012-12-20 11:48:00 GMT

perlfilterの例をリーダーマクロで実装してみる

今回は、Perlのリーダーマクロ的なものである、Source Filtersのドキュメントにある例をCommon Lispに移植しつつ、何か発見があるかを探ってみます。

USING CONTEXT: THE DEBUG FILTER

Source Filtersの活用の例として、コメント内にデバッグ用のプリントを仕込んでおいてフィルターが有効の時は、デバッグプリントが出力され、有効でない場合は、ただのコメントとして読み飛ばされる、というものが紹介されています。
具体的な仕様ですが、
    ## DEBUG_BEGIN
    #if ($year > 1999) {
    #     warn "Debug: millennium bug in year $yearn";
    #}
    ## DEBUG_END
のようにDEBUG_BEGIN行からDEBUG_END行までがフィルター利用時には活性化されます。
Common Lispだと行コメントは、『#』ではなく『;』になりますが、『;』の設定をカスタマイズすれば実現できそうです。
ということで、Perlのコードを参考にしつつ書いてみました。
(defun debug-reader (srm char)
  (unread-char char srm)
  (let ((in-debug nil))
    (loop :for line := (if (eql #\; (peek-char t srm nil nil T))
                           (read-line srm nil srm T)
                           srm)
          :until (or (and (eq line srm) 
                          (if in-debug
                              (error "DEBUG_BEGIN has no DEBUG_END")
                              t))
                     (and (ppcre:scan "^\\s*;+\\s*DEBUG_END\\b.*" line)
                          (or in-debug (error "DEBUG_END has no DEBUG_BEGIN"))))
          :when (ppcre:scan "^\\s*;+\\s*DEBUG_BEGIN\\b.*" line)
            :do (if in-debug
                    (error "Nested DEBUG_BEGIN")
                    (setf in-debug T))
          :else 
            :when (and in-debug (stringp line)) 
              :collect (ppcre:regex-replace "^\\s*;+" line "") :into ans
          :finally (return 
                     (let ((ans (apply #'concatenate 'string ans)))
                       (if (< 0 (length ans))
                           (read-from-string (format nil "(progn ~A)" ans))
                           (values)))))))
これを、
(set-macro-character #\; #'debug-reader)
なりなんなりします。
(let ((year 2000))
  ;; 1
  ;; 2
  ;; 3
  ;; 4
  year)
;=>  2000
(let ((year 2000))
  ;; bar
  ;; foo
  ;; DEBUG_BEGIN
  ;; (if (> year 1999)
  ;;     (warn "Debug: millennium bug in year ~A" year))
  ;; (if (> year 1999)
  ;;     (warn "Debug: millennium bug in year ~A" year))
  ;; DEBUG_END
  ;; bar
  ;; foo
  ;; DEBUG_BEGIN
  ;; (if (> year 1999)
  ;;     (warn "Debug: millennium bug in year ~A" year))
  ;; DEBUG_END
  year)
;>> WARNING: Debug: millennium bug in year 2000
;>> WARNING: Debug: millennium bug in year 2000
;>> WARNING: Debug: millennium bug in year 2000
;=>  2000
こういうマクロ文字を設定すると、一体どういう風に展開されるのか確かめたくなりますが、SLIMEのようなものであれば、マクロ展開コマンド(slime-macroexpand-1)が割と使えます。単に式をreadできればなんでも良いのですが、readするだけのコマンドもないので代用というところです。
;; 上記を展開してみたところ
(LET ((YEAR 2000))
  (PROGN
   (IF (> YEAR 1999)
       (WARN "Debug: millennium bug in year ~A" YEAR))
   (IF (> YEAR 1999)
       (WARN "Debug: millennium bug in year ~A" YEAR)))
  (PROGN
   (IF (> YEAR 1999)
       (WARN "Debug: millennium bug in year ~A" YEAR)))
  YEAR)

まとめ

以上、今回は、Source Filtersの例をCommon Lispに移植しつつ眺めてみました。
基本的に文字列をパーズするだけなので、どちらかというと文字列をどう読み込んで処理するかの違いが出てくる(Perlだと行指向+正規表現、Common Lispだとread+α)だけ、という感じです。ストリームの行に対して正規表現でマッチをかけて、そこから読んだり、読み飛したりできると便利かもしれません(既にやってる人がいたようないないような…)。
似たような機能を持つ言語からアイデアを借りてみるのは色々面白いと思うので一度試してみては如何でしょうか。

手軽なTipsとしてのリーダーマクロ

Posted 2012-12-19 06:32:00 GMT

手軽なTipsとしてのリーダーマクロ

今回は、ちょっと趣向を変えて、日常的にLispを使っているなかで遭遇する問題でリーダーマクロがたまに活躍する場面を紹介したいと思います。

[1,2,3,4,5]をLispのリストとして読む

他の言語のサンプルコードなどで、[1,2,3,4,5]のような表記は多いのですが、そのままLispで読むには『,』等が邪魔です。Clojureでは『,』を空白として読まれるので、[1,2,3,4,5]は、[1 2 3 4 5]と読まれます。これに倣って『,』を空白に設定して読んでみましょう。
(defun read-list (endchar)
  (lambda (s c)
    (declare (ignore c))
    (read-delimited-list endchar s T)))

(let ((*readtable* (copy-readtable nil))) (set-syntax-from-char #\, #\Space) (set-macro-character #\[ (read-list #\])) (set-syntax-from-char #\] #\)) ;; (read-from-string "[1,2,3,4,5,6,7,8,9]")) ;=> (1 2 3 4 5 6 7 8 9) ; 19

(let ((*readtable* (copy-readtable nil))) (set-syntax-from-char #\, #\Space) (set-macro-character #\[ (read-list #\])) (set-syntax-from-char #\] #\)) (read-from-string "[1,2,3,4,5,6,7,8,[9,10,11],[[[[[[[12]]]]13,14]]]]")) ;=> (1 2 3 4 5 6 7 8 (9 10 11) (((((((12)))) 13 14)))) ; 49

[]が入れ子になっていても適切に解釈してくれるので楽です。
予めこういうリードテーブルを用意しておいて、雑務で必要になったら、さっと使うというのも良いですね。リーダーマクロは使わずエディタで置換するのも良いと思います。

全角の空白が邪魔だ!!

たまにですが、コピペしてきたコードや、ファイル内に全角の空白が混っていたりして、ロードするとエラー起したりします。これも全角の空白を、ASCIIの空白と同じ意味にすることにより回避できます。
(let ((*readtable* (copy-readtable nil)))
  (set-syntax-from-char #\  #\Space)
  (read-from-string "( aaa  aaa)"))
;=>  (AAA AAA)
;    11

(let ((*readtable* (copy-readtable nil))) (set-syntax-from-char #\  #\Space) (load "/tmp/foo.lisp"))

こういうものをプログラミングの道具箱に潜ませて置くもの良いのではないでしょうか。リーダーマクロは使わずエディタで置換するのも良いと思います。

Common Lispでシンボル名がすべて大文字化されるのを防ぎたい

Common Lispだとシンボル名は、デフォルトでは大文字にupcaseされて読まれます。大文字小文字の段の違いをどうしてもそのままにしたい場合は、リーダーマクロによって段を保持したまま読み込むのも手です。
下記の例では、#_以降の一つの式は大文字化しないで読みます。
(set-dispatch-macro-character #\# #\_
                              (let ((rt (copy-readtable nil)))
                                (setf (readtable-case rt) :preserve)
                                (lambda (s c a)
                                  (declare (ignore c a))
                                  (let ((*readtable* rt))
                                    (read s t nil t)))))

#_(DEFUN Fibonacci (n) (IF (< n 2) n (+ (Fibonacci (1- n)) (Fibonacci (- n 2)))))

(|Fibonacci| 10) ;=> 55

(list '#_FooBarBaz '#_FooBarBaz) ;=> (|FooBarBaz| |FooBarBaz|)

SLIMEではSLIME側からemacsにアクセスできたりしますが、こういう場合にも段の扱いの違いの解消に便利です。
(swank:eval-in-emacs 
 '#_(progn
      (defun foo-test () 
        (interactive "")
        (message "test!!"))
      0))
ちなみに、CCLには標準で#_が定義されているのですが、それの段を保持する部分だけを参考にしてみました。主に、Cとのスムーズな連携のため、#_を使って段の保持をしつつ、さらに色々展開したりしているようです。

まとめ

以上、今回は、手軽なTipsとしてのリーダーマクロを紹介してみました。
基本的に空白にすげ変えるのしか紹介してなくね?という声もありそうです。
ちょっとした工夫で、楽ができることもありますので、試してみては如何でしょうか。

リーダーマクロでコードのコピペ

Posted 2012-12-18 10:37:00 GMT

リーダーマクロでコードのコピペ

Common LispやSchemeでは、構文にラベルを付けることが可能です。これを利用して重複したコードをまとめて記述することが可能です。

『#=・##』

Common Lispでは標準で、Schemeでは、SRFI 30や、R7RSで#=・##を利用することが可能です。
Common Lispの解釈としては、#n=は、#=が引数nを持つという解釈で、##も同じです。
#n=がラベルを付け、#n#で参照します。
(list #1='hello #0='world #1# #1# #0#)
;=>  (HELLO WORLD HELLO HELLO WORLD)
最も多い使われ方は、リストの等の共有構造や循環構造を直に記述する場合かと思います。
(mapcar #'list
        '(a b c d e f g h i j k)
        '#0=(1 2 3 4 . #0#))
;=>  ((A 1) (B 2) (C 3) (D 4) (E 1) (F 2) (G 3) (H 4) (I 1) (J 2) (K 3))
上記の場合、先頭のセルに#0#というラベルを付け、最後のセルのcdrが#0#を参照していて単純なループ構造になっています。
+------------------------------------------------+
+-----------------#0#-------------------+
v
+---+---+ +---+---+ +---+---+ +---+-+-+
* * --> * * --> * * --> * *
+---+---+ +---+---+ +---+---+ +---+---+
v v v v
+---+ +---+ +---+ +---+
1 2 3 4
+---+ +---+ +---+ +---+
+------------------------------------------------+

ラベル機能を利用して重複した記述をまとめる

たまに、二箇所で全く記述があり、どうにか纏められないかな、ということがありますが、こういう場合に#=・##でまとめることが可能です。
(baz (eggs (baz)
           (fuga eggs)
           (foo tata foo))
     (eggs (baz)
           (fuga eggs)
           (foo tata foo)))
の様なものの場合は、
(baz #999=(eggs (baz)
               (fuga eggs)
               (foo tata foo))
     #999#)
のようにまとめられます。
(foo (bar 1 2 3 4 5 6 7 8 9 0)
     (baz 1 2 3 4 5 6 7 8 9 0)
     1 2 3 4 5 6 7 8 9 0)
のようにフォームのCDR部が同じ場合も、工夫すれば、
(foo (bar . #0=(1 2 3 4 5 6 7 8 9 0))
     (baz . #0#)
     . #0#)
のようにも書けたりします。
これらのメリットとしては、修正が必要になっても修正が一箇所で済む、ということがありますが、デメリットとしては、多用すると数字の記号が謎の呪文のようで読み辛いことが挙げられるかなと思います。

練習問題 22. なにかラベル付けで便利な使いかたを考えてみましょう

『#L=・#L#』

MacLISPでは、(progn #Lfoo=x #Lfoo#)という風に数字でなく名前で参照できたようですが、詳細不明です。確かに数字より名前を付けられた方が便利ではあるかなと思いますが…。

まとめ

以上、今回は、リーダーマクロでコピペを紹介しました。
慣れてくると複雑なことをしたくなって来るのですが、そこそこに抑えておいた方が良いかなと思います。また、リード時に評価されるので、極力変数の参照などは行なわない方が懸命かなと思われます。
リーダーマクロでコードをラベル付けしてコピペのようなことをすることについては、他の言語に似たものがないこともあり、利用のガイドライン的な物がありません。恐らく多用すべきてはないだろうと思われますが、Let Over Lambdaの書籍中のように多用しているもスタイルもあったりはします。新しい可能性として追求してみるのも面白いかもしれません。

Older entries (1532 remaining)