请教:SET-MACRO-CHARACTER 可否读取前一个字符

以前我也在此发过一个帖子,那时才意识到 SET-MACRO-CHARACTER 的魅力,然后我就开始了定义自己的Style(Lisp不那么广为流传的原因吧,太灵活了。。。),对于一个只有一个执行语句的lambda就简单一下,让参数默认为 $1, $2, $n,对于把所有参数定位一个&rest 的时候,就用 %(…*),这样让代码更简洁。。。:

$(+ a b $1) => (lambda ($1) (+ a b $1)   
%(car *) => (lambda (&rest *) (car *)

但后来发现有个地方报错,发现是 Hunchentoot 框架有些函数名是 % 结尾的(handle-incoming-connection%)
所以我现在只能用 #% 的形式了: SET-DISPATCH-MACRO-CHARACTER
虽然只是多输入一个字符# 但是感觉没那么完美。。。
所以想请教下在 SET-MACRO-CHARACTER 里能否读取(偷窥?peek)前一个字符(我只能 read 后面的啊)?

另外我还是有点洁癖,我们经常要传递一个与key同名的参数 例如 (find … :test test) 的test,能否实现只用一个呢? 我也想尝试用 :test (find ... :test) ,但只能生成 (:test test),因为macro只能生成一条语句,所以只能合并到一个 list 里。。。如果要借用 apply 那也没有意义

我Google了好久都找不到解决方案,不知道是因为没有人觉得这个有必要还是这个是实现不了的~

具体实现:

(defun dollar-sign (stream &rest chars)
      "$(+ 1 $1) => (^($1) (+ 1 $1)
  $abc => |$ABC| (as normal)
  ab$c or abc$ are illegal"
      (declare (ignore chars))
      (let ((code (read stream)))
        (if (atom code)
            (if (keyword? code)
                (list 'list code (key-to-symbol-symbol code))
                ;; (append '(apple) $:test) => (apple :test test)
                ;; (list 'apple $:test) => (apple (:test test))
                (intern (string-upcase (input "$~s" code))))
            (parse-to-lambda code))))

    ;; (set-macro-character #\$ #'dollar-sign)
    (set-dispatch-macro-character #\# #\$ #'dollar-sign)

    
    (defun percent-sign (stream &rest chars)
      ;; (call  #%(list @ 4 5 6) 1 2 3)    => ((1 2 3) 4 5 6)
      ;; (apply #%(list @ 4 5 6) '(1 2 3)) => ((1 2 3) 4 5 6)
      ;; (apply #%(append @ '(4 5 6)) '(1 2 3)) => (1 2 3 4 5 6)  
      (declare (ignore chars))
      (let ((code (read stream)))
        (if (atom code)
            (intern (string-upcase (input "%~s" code))) ;; As normal
            `(^(&rest *) ,code))))
    (set-dispatch-macro-character #\# #\% #'percent-sign))

也把我积累的通用代码库放到了 GitHub里

不能。除非用另外实現的 reader。然而就这个需求根本犯不着这么做,见下一夈。

你不知道 readtable 啥用么?

CL-USER> *readtable*
#<READTABLE #x3020002FB70D>
CL-USER> (setf tmp (copy-readtable))
#<READTABLE #x302000BD923D>
CL-USER> (set-macro-character #\% (lambda (s c) (error "not work")))
T
CL-USER> 'foo
FOO
CL-USER> 'foo%
; Debugger entered on #<SIMPLE-ERROR #x302000BF758D>
Error: not work
CL-USER> (setf *readtable* tmp)
#<READTABLE #x30200096F20D>
CL-USER> 'foo%
FOO%

管理 readtable

只能说明你的设计有问題,沒有运用 special variable。

我知道,所以我也写了一个 $cancel-macor-char

(defun $cancel-macor-char (char)
  (set-macro-character char nil))

但是我想把我这个代码库——包括我这个语法,“分享” 到 GitHub,所以,想更灵活点(用户也可以cancel掉这个语法)。。。
至少我后面的项目会要load我这个库然后用这套风格的。。。

不是很理解,求赐教,给个栗子?(special variable我也算是琢磨了一段时间啊,例如觉得 symbol-macrolet 这些也会有用处😂)

terminating n. (of a macro character) being such that, if it appears while parsing a token, it terminates that token. See Section 2.2 (Reader Algorithm).

是否 “terminating” 和 po 主要求的行为无关

然而

CL-USER> '%
%
CL-USER> (set-macro-character #\% (lambda (a b) (error "notwork")) t)
T
CL-USER> '%
Error: not work
CL-USER> '%a
Error: not work

而 CL 用有以 % 作为开头以表示这是个 internal only 的习慣。

(apropos "%")
...
                     CCL::%TYPED-UVSET
                    CCL::%%TYPEP, Def: FUNCTION
                     CCL::%TYPEP, Def: FUNCTION
                     CCL::%TYPESPEC-ID, Def: FUNCTION
                     CCL::%UB-FIXNUM-HEX-DIGITS, Def: FUNCTION
                     CCL::%UNADVISE-1, Def: FUNCTION
                     CCL::%UNADVISE-ALL, Def: FUNCTION
                     CCL::%UNAME, Def: FUNCTION
                     CCL::%UNARY-ROUND, Def: FUNCTION
                     CCL::%UNARY-ROUND-TO-FIXNUM, Def: FUNCTION
                     CCL::%UNARY-TRUNCATE, Def: FUNCTION
...

这就是为什么要用 named readtable 在 readtable 里想怎么改就怎么改而不是试图一定要和現有习慣兼容。

你不懂 CL 就不要瞎掺合了

你的方案治标不治本

non-terminating-p 确实解决问题了! 因为我看那文档没有示例代码,所以没留意到这个参数~
http://www.lispworks.com/documentation/HyperSpec/Body/f_set_ma.htm

这个我不知道啊,我也是考虑筛选过用什么字符,可以选择的并不多,除了 %就 @, &了~~
^我已经拿来用了 就是lambda 的代名词 (^(x) (+ 1 x) …

所以你应该直接用 named readtable 管理,这样別人写的代码用标准的 readtable 还可以确保正常加載。別人写的用 reader macro 的代码也可以用他们自定义的 readtable 加載,更不会有冲突。

给你看几个使用 reader macro 的项目做下范例

你想搞自己的扩展那 CL21 更是要看一下,虽然凉了但是积累了很多经验。

CL21 针对这种需求有设置 ASDF 自动配置 readtable 你可以看下

Yes, sir!
我一开始是想不仅仅是这个package 才用这语法,其它引用这个package的也用,所以弄成global readtable

(defun fn1 (a &key b c)
  (+ a b c))

(let ((a 1)
      (b 2)
      (c 3))
  (fn1 a :b b :c c))

(defun fn2 (a &optional (b *b*))
  (declare (special *c*))
  (+ a b *c*))

(let ((a 1)
      (*b* 2)
      (*c* 3))
  (declare (special *b* *c*))
  (list (fn2 a) (fn2 23 44)))

有时候用 fn2 这样更好。*b* 有个在实践中很常見的例子叫 *standard-output*

你也可以把 special variable 当成 reader monad 运用和理解 (此 reader 非佊 reader)。

你这个例子我能理解(好像看起来没有那么常规的直观)
不过我请教的主要是在调用的时候 我们经常是 :key key 这样重复的名字。。。
例如 PCL 第24章的一个例子:

看起来好像就不够灵活 如果能够变成 $:id 或者 #:size 是不是看起来简洁点?(也不一定)

我也是看了一小部分,刚开始是觉得很高端,现在慢慢知道原理怎么实现了,
set-macro-char/set-dispatch-macro-character 用得精。。。(记得有个成语形容取其精髓充分利用,想不出来了,唉大脑的内存不够用了)

我看这里 Common Lisp 的贴并不多,不好意思一下开那么多个啊

不好意思,我理解成 Special Operators 了。。。

这里更正交的做法不应該是用 threading macro 么?

(as->
  (->> (read-value ...) ;; id
       (->> (read-value ...) ;; value
            ^(make-instance (find-frame-class id) :id %2 :size %1)))
  $
  (read-object $ stream))

threading macro 并不懂啊~(多线程?PCL好像没提到啊。。。)
你这代码算是勉强看懂了(不过是不是最后要个返回值$对应他这里的object),但可能我还不习惯,暂时觉得有点生涩,不过我们都是想要提高代码的可读性,多层嵌套看起来是比较复杂

(defmethod read-value ((type (eql 'id3-frame)) stream &key)
  (let ((id (read-value 'iso-8859-1-string stream :length 3))
        (size (read-value 'u3 stream)))
    (let ((object (make-instance (find-frame-class id) :id id :size size)))
      (read-object object stream)
      object)))

我想能不能变成:

(defmethod read-value ((type (eql 'id3-frame)) stream &key)
  (let ((id (read-value 'iso-8859-1-string stream :length 3))
        (size (read-value 'u3 stream)))
    (let ((object (make-instance (find-frame-class id) $:id $:size)))
      (read-object object stream)
      object)))
 :id id :size size =>  $:id $:size

你不能通过前缀实现 但是能通过后 后缀就需要你得到前一个已经read的值

有两个办法可以做到 你可以新建一个可以read-last的stream 用一个首尾重合的双链表存储最近读过的char

你还可以hack一下read函数 让一个全局变量保存最近的值 但是太低效

我才发现你的两个问题 其实是一个问题 unread

它好像不支持unicode吧 我不懂cl 乱猜的

不过从readtable的255看的出来是不支持的

那样的话 数学符号就用不了了吧 比如 forall

有人研究过让它支持符号吗? 没问题的吧应该 都是大神级的人物 这个小问题应该摆的平

不是很懂你的意思,能否写几行示例代码更好理解?