newLISP® 代码模式

Version 2012 February 14th
newLISP v.10.4.0



Copyright © 2012 Lutz Mueller, www.nuevatec.com. All rights reserved.
Chinese translations copyright © 2012 short story 黄登(winger)

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License,
Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts,
and no Back-Cover Texts. A copy of the license is included in the section entitled GNU Free Documentation License.

newLISP is a registered trademark of Lutz Mueller.



§

1. 序

在人们使用 newLISP 的过程中, 总会有某些构思和问题频繁的出现. 久而久之这些想法被汇集成了这本手册.

手册的部分内容和用户手册重叠.

手册对部分函数的功能和编码方式做了更深入的讲解.

本手册将持续更新收集到的代码模式,和各类问题的解决方法.


§

2. newLISP 脚本操作

命令行参数

On Linux/Unix, 将下面的代码写在脚本文件的第一行:

#!/usr/bin/newlisp

指定一个更大的堆栈:

#!/usr/bin/newlisp -s 100000

或者

#!/usr/bin/newlisp -s100000

不同系统的Shell对参数的分析和处理方式是不一样的.newLISP可以处理联合和分开两种参数传递方式(如上).下面这段简单的代码可以用来测试各种系统的参数处理方式. 脚本将堆栈设置成100,000 ,内存最大占用量限制在10M以内.

#!/usr/bin/newlisp -s 100000 -m 10
     
(println (main-args))
(println (sys-info))

(exit) ; important

下面是某个系统的输出数据:

./arg-test
     
("/usr/bin/newlisp" "-s" "100000" "-m" "10" "./arg-test")
(308 655360 299 2 0 100000 8410 2)

记住大部分程序是不需要修改堆栈参数的; 大部分情况下默认的2048已经够用了. 每个堆栈平均消耗80个字节. 其余的newLISP参数选项,爬手册去吧^_^.

管道交互

下面代码展示了如何将一个文件通过管道输入到newLISP 脚本中.

#!/usr/bin/newlisp
#
# uppercase - demo filter script as pipe
#
# usage:
#          ./uppercase < file-spec
#
# example:
#          ./uppercase < my-text
#
#
     
(while (read-line) (println (upper-case (current-line))))
     
(exit)

文件内容将会被转换成大写然后打印到标准输出 (std-out).

下面的代码能够处理非文本的二进制文件(包含了0字符):

#!/usr/bin/newlisp
;
; inout - demo binary pipe
;
; read from stdin into buffer
; then write to stdout
;
; usage: ./inout < inputfile > outputfile
;

(while (read 0 buffer 1024)
    (write 1 buffer 1024))

(exit)

将缓冲区大小设置到最高效的尺寸.

文件过滤

下面的脚本类似Unix下的 grep , 循环读取每一个文件,并且使用正则表达式测试文件中每一行的内容,找到含有匹配内容的行就打印出来.

#!/usr/bin/newlisp
#
# nlgrep - grep utility on newLISP
#
# usage:
#          ./nlgrep "regex-pattern" file-spec
#
# file spec can contain globbing characters
#
# example:
#          ./nlgrep "this|that" *.c
#
# will print all lines containing 'this' or 'that' in *.c files
#
     
(dolist (fname (3 (main-args)))
    (set 'file (open fname "read"))
    (println "file ---> " fname)
    (while (read-line file)
        (if (find (main-args 2) (current-line) 0)
            (write-line)))
    (close file))
    
(exit)

表达式:

(3 (main-args))

相当于:

(rest (rest (rest (main-args))))

返回一个包含了所有文件的列表. 这种数字在前的方式就是传说中得隐式索引. 详细的内容看用户手册. 表达式 (main-args 2) 提取命令行的第三个参数作为正则表达式.


§

3. 模块化编码

构造一个程序

当你的代码比较多或者各个代码文件之间充斥着大量重复代码的时候, 就必须把这些冗余的代码划分进一个个不同的代码模块(否则你很可能写出"九阴真经"). newLISP中使用context来创建模块, 也就是命名空间. 命名空间将各个模块之间的词法隔离开. 这样各模块可以拥有相同名字的变量而不冲突.

一个模块文件包含数据库访问代码.

; database.lsp
;
(context 'db)
   
   
(define (update x y z)
...
)
   
(define (erase x y z)
...
)

另一个模块包含数据操作代码.

; auxiliary.lsp
;
(context 'aux)
 
(define (getval a b)
...
)

最后他们在唯一的至高无上的MAIN模块中被调用剩下的就是XXOO的伟大建设 (MAIN 模块是默认的系统模块 类似国家首都):

; application.lsp
;
   
(load "auxiliary.lsp")
(load "database.lsp")
   
(define (run)
    (db:update ....)
    (aux:putval ...)
    ...
    ...
)
   
(run)

一个文件多个模块

当然如果你很强大很喜欢AIO ,也可以把所有模块写入一个文件(再次警戒三妻四妾不可同房-_-!).不过要以 (context MAIN) 结束每个模块(否则你永远都回不到我们的伟大首都了):

; myapp.lsp
;
(context 'A)
   
(define (foo ...) ...)
   
(context MAIN)
   
(context 'B)
   
(define (bar ...) ...)
   
(context MAIN)
   
(define (main-func)
    (A:foo ...)
    (B:bar ...)
)

注意在创建context AB 的时候,他们的名字前都有单引号,这是因为他们是新创建的. 但是 MAIN 不用,因为他是 newLISP 在启动时自己创建的. 当然, 给他加个引号也没有问题.

凡事都是技巧 (context MAIN) 也可以用下面的代码替换:

; myapp.lsp
;
(context 'A)
   
(define (foo ...) ...)
   
(context 'MAIN:B)
   
(define (bar ...) ...)
   
(context 'MAIN)
   
(define (main-func)
    (A:foo ...)
    (B:bar ...)
)

(context 'MAIN:B) 回到 MAIN 并创建一个新的context B.

默认函数

每一个模块都可以有一个和本模块同名的函数,这个就是默认函数. 这个函数有着特殊的作用:

(context 'foo)
  
(define (foo:foo a b c)
...
)

函数 foo:foo 就是foo的 默认函数, 调用 foo 的时候就像调用一个, 他将会被自动的映射到 foo:foo

(foo x y z)
; same as
(foo:foo x y z)

默认函数看起来和普通函数一样, 但是他们可以操作自己独立的词法空间(内部的数据和外部的独立开来). 利用这个特性,默认函数可以很方便的保存各种状态数据(因为每个命名空间是独立开的):

(context 'generator)
   
(define (generator:generator)
    (inc acc)) ; when acc is nil, assumes 0
 
(context MAIN)
   
(generator) → 1
(generator) → 2
(generator) → 3

下面是个更复杂的例子,用来生成下面是个更复杂的例子,用来生成斐波纳契序列:

(define (fibo:fibo)
    (if (not fibo:mem) (set 'fibo:mem '(0 1)))
    (last (push (+ (fibo:mem -1) (fibo:mem -2)) fibo:mem -1)))
   
(fibo) → 1
(fibo) → 2
(fibo) → 3
(fibo) → 5
(fibo) → 8
...

上面的代码在 on-the-fly (实时)状态下直接申明了一个默认函数,并没用 context 显示的申明一个名字空间. 用context显示申明也是同样可以的(更多context信息请爬手册):

(context 'fibo)
(define (fibo:fibo)
        (if (not mem) (set 'mem '(0 1)))
        (last (push (+ (mem -1) (mem -2)) mem -1)))
(context MAIN)
  
(fibo) → 1
(fibo) → 2
(fibo) → 3
(fibo) → 5
(fibo) → 8

第一段代码更短,第二段的可读性更好(吃熊掌的跟我走啊).

用context封装数据

前面的例子都是使用函数将数据封装在不同命名空间中. 在 generator 的例子中 acc 用来保存状态. 在 fibo 的例子中用 mem 来保存增长的列表. 两个例子里,函数和变量结合在了一起. 下面的例子中直接将数据赋值给默认函数,而不使用函数(其实function is data):

(set 'db:db '(a "b" (c d) 1 2 3 x y z))

就像使用 fibogenerator 那样,我们可以用 db来调用 db:db . 此时db就像普通list一样,可以完成任意的列表操作:

(db 0)    → a
(db 1)    → "b"
(db 2 1)  → d
(db -1)   → z
(db -3)   → x
   
(3 db)    → (1 2 3 x y z)
(2 1 db)  → ((c d))
(-6 2 db) → (1 2)

用引用传递对象

当默认函数被作为某个函数的参数调用的时候, 默认函数会通过引用传递. 这意味这传递给函数的参数是原始数据(别去想那邪恶的指针God!), 而不是列表或者字符串的拷贝. 在传递庞大的数据的时候这个特性非常有用:

(define (update data idx expr)
    (if (not (or (lambda? expr) (primitive? expr)))
        (setf (data idx) expr)
        (setf (data idx) (expr $it))))
   
(update db 0 99) → a
db:db → (99 "b" (c d) 1 2 3 x y z)
   
(update db 1 upper-case) → "b"
db:db → (99 "B" (c d) 1 2 3 x y z)
   
(update db 4 (fn (x) (mul 1.1 x))) →
db:db → (99 "B" (c d) 1 2.2 3 x y z)

db:db 中的数据通过变量 data 传递给函数 update , 现在update就拥有一个 context db的数据引用. 接着判断传入的 expr 参数是否是系统函数,操作符或者用户定义的函数的,如果是的话就使用expr 操作 $it , $it是一个系统变量包含了表达式(这里是setf)要操作的原始数据(data idx).

当一个newLISP函数 需要一个列表或者在字符串参数的时候, 使用context名就能将默认函数传递进去. 了一个例子:

(define (pop-last data)
(pop data -1))
   
(pop-last db) → z
   
db:db         → (99 "B" (c d) 1 2.2 3 x y)

update 同时也展示了如何将函数作为数据传递给别的函数 (upper-case 也依靠 $it). 更多内容请看 函数即数据.


§

4. 局部变量

循环函数中得局部变量

所有的循环函数 doargs, dolist, dostring, dotimes, dotree for 都使用局部变量. 在循环结构内部,每一次循环,局部变量都能获得不同的值. 但是在出了循环以后,循环内部的局部变量将消失 . let, define, 和 lambda 是另一种申明局部变量的方法:

let local 申明局部变量

let 是一种常见的申明本地变量的方法.

(define (sum-sq a b)
    (let ((x (* a a)) (y (* b b)))
        (+ x y)))
 
(sum-sq 3 4) → 25
 
; alternative syntax
(define (sum-sq a b)
    (let (x (* a a) y (* b b))
        (+ x y)))
 
; using local
(define (sum-sq a b)
    (local (x y)
        (set 'x (* a a))
        (set 'y (* b b))
        (+ x y)))

变量x和y先被初始化然后,执行表达式(+ x y). let 结构其实就是下面语句的优化版本(exp-body 也作为参数传递进去喽,最后还要eval exp-body滴):

((lambda (sym1 [sym2 ...]) exp-body ) exp-init1 [ exp-init2 ...])

当初始化的变量相互关联的时候, 一个嵌套的 let, letn 就能够发挥作用了,之前初始化过的局部变量可以被应用到 随后变量 的初始化中:

(letn ((x 1) (y (+ x 1)))
    (list x y))              → (1 2)

函数 locallet 很像不过他将所有变量初始化成 nil.

未使用的参数做局部变量

在 newLISP 里, 所有用户定义的函数的参数都是非强制赋值的. 未使用(为赋值)的参数会被自动赋值为 nil , 所有的参数的作用域都局限在本函数内. 定义一个函数然后在参数列表后加几个用不到的参数,这些参数就能过作为函数的局部变量使用了(这样就可以摆脱let中无数多的括号 爽不~_~):

 
(define (sum-sq a b , x y)
    (set 'x (* a a))
    (set 'y (* b b))
    (+ x y))

逗号不是专门的语法,他只在视觉上将函数参数和局部变量区分开. (本质上说 逗号, 和 x , y 一样, 也是一个局部变量,也被初始化成nil.) (天地无道,皆发于心)

参数默认值

定义函数的时候能够制定参数的默认值:

(define (foo (a 1) (b 2))
    (list a b))
  
    (foo)      →  (1 2)
    (foo 3)    →  (3 2)
    (foo 3 4)  →  (3 4)

args 替代 local

args 函数的好处就是可以省去参数的显示申明, args 返回一个包含了所有参数值的列表(已经显示申明的参数除外):

(define (foo)
    (args))
   
(foo 1 2 3)   → (1 2 3)
   
   
(define (foo a b)
    (args))
   
(foo 1 2 3 4 5)   → (3 4 5)

第二个例子中 args 返回的参数值列表,排除了赋值给 a b 的参数值.

通过索引可以访问 (args) 列表:

(define (foo)
      (+ (args 0) (args 1)))
   
(foo 3 4)   → 7

bind 绑定指定的参数

bind 可以和 local , args 配合起来对指定的参数赋值(很像之前讲的参数默认值哦@_@):

(define-macro (foo)
    (local (len width height)
        (bind (args) true)
        (println "len:" len " width:" width " height:" height)
))
       
     
> (foo (width (+ 15 5)) (height 30) (len 10))
len:10 width:20 height:30
     
> (foo (width (+ 15 5)) (height (* width 2)) (len 10))
len:10 width:20 height:40
       
>

参数在局部计算然后赋值给局部变量.

注意,这个宏不能通过命名空间调用,因为命名空间里申明的所有symbol,都必须加上空间名(xxx:len),否则只能bind到MAIN里的变量.


§

5. 遍历列表

递归还是迭代?

虽然递归为很多算法提供了良好的可读性, 但在某些情况下也是非常低效的. newLISP 有很多的迭代构造器和高层控制函数,比如 flat 或者系统自带的 XML 函数, 其内部使用递归. 这样一来,很多时候就不用自己定义一个新的递归函数了(不用白不用啊^_^).

有时候一个非递归的解决方案会比递归的更快耗费的资源也更少.

; classic recursion
; slow and resource hungry
(define (fib n)
    (if (< n 2) 1
        (+  (fib (- n 1))
            (fib (- n 2)))))

上面这个递归的方案就非常非常慢(各位可以传递个100试试),为什么会这样呢,频繁的系统开销,加上大量的内存操作全部消耗在临时变量和每次递归的冗余结果上..

; iteration
; fast and also returns the whole list
(define (fibo n , f)
    (set 'f '(1 0))
    (dotimes (i n)
         (push (+ (f 0) (f 1)) f)) )

而这个迭代的版本不仅快内存消耗也小(对了最好别用递归,nP里太多了是会overflow的,满则溢啊 xxoo.....).

用 memoization 提速

memoizing 函数的作用就是缓存函数的执行结果,这样下次用同样的参数调用函数的时候就不用执行,直接可以检索到了 . 下面这个函数(宏)的作用就是,为任何一个系统自带或者是用户创建的函数创建一个 memoizing 函数 ,而且不限制你原来函数的参数. 当你用这个宏创建了一个函数的memoizing 函数后,再调用这个memoizing 函数,他就会创建一个专门命名空间用来存储数据(下面这个宏核心就是(set x (letex )) , 宏的作用主要就是用来生成函数的,宏和一般函数一个非常明显的不同点就是参数不执行(eval),至于 letex 和 sym 不熟悉的同学 爬爬手册吧 ) .

; speed up a recursive function using memoization
(define-macro (memoize mem-func func)
    (set (sym mem-func mem-func)
        (letex (f func  c mem-func)
          (lambda ()
              (or (context c (string (args)))
              (context c (string (args)) (apply f (args))))))))
       
(define (fibo n)
    (if (< n 2)  1
        (+  (fibo (- n 1))
            (fibo (- n 2)))))
       
(memoize fibo-m fibo)
       
(time (fibo-m 25)) → 148
(time (fibo-m 25)) → 0

函数创建了一个新的 context 和 默认函数, 名字换成新函数的名字(上面是fibo-m) 当然最后还是要调用原函数来执行的,新函数执行的结果呢就全部存在这个新的context里.

在 memoizing 递归函数的时候, 在原函数的位置直接用(lambda)调用新函数也是可以的:

(memoize fibo
    (lambda (n)
        (if(< n 2) 1
            (+  (fibo (- n 1))
                (fibo (- n 2))))))
        
(time (fibo 100)) → 1
(fibo 80)         → 37889062373143906

最后一例子调用原始 fibo 会花上数小时. 而 memoized 版本 仅花费了一毫秒 (OMG 苍天啊 大地啊).

遍历树

无论传统 LISP 还是 newLISP 遍历树都是一种很典型的模式,所要面对的都是遍历嵌套列表. 但在很多情况下只需要迭代遍历一个已经存在的树(嵌套列表). 这时候内建的 flat 函数就比递归快多了:

(set 'L '(a b c (d e (f g) h i) j k))
   
; classic car/cdr and recursion
;
(define (walk-tree tree)
    (cond ((= tree '()) true)
          ((atom? (first tree))
             (println (first tree))
             (walk-tree (rest tree)))
          (true
             (walk-tree (first tree))
             (walk-tree (rest tree)))))
   
; classic recursion
; 3 times faster
;
(define (walk-tree tree)
    (dolist (elmnt tree)
        (if (list? elmnt)
            (walk-tree elmnt)
            (println elmnt))))
   
(walk-tree L) →
     a
     b
     c
     d
     e
     ...

用newLISP自带的 flat 函数能够将多层的嵌套列表转换成一层的普通列表.现在列表就能够用 dolistmap 操作了:

; fast and short using 'flat'
; 30 times faster with map
;
(map println (flat L))
 
; same as
 
(dolist (item (flat L)) (println item))

遍历目录树

在遍历目录树方面递归表现的更好:

 
; walks a disk directory and prints all path-file names
;
(define (show-tree dir)
    (if (directory dir)
        (dolist (nde (directory dir))
            (if (and (directory? (append dir "/" nde))
                     (!= nde ".") (!= nde ".."))
                (show-tree (append dir "/" nde))
                (println (append dir "/" nde))))))

在这个例子中,递归是唯一的方法, 因为整个文件名的嵌套列表在函数开始执行的时候是无法获得的, 只能在每递归一次获得一部分.


§

6. 修改和搜索列表

在 newLISP 里我们可以很方便的通过多维索引操作嵌套列表. 有些函数是具有 破坏性的 push, pop, setf, set-ref, set-ref-all, sortreverse ,还有些是 非破坏性的 , 像 nth, ref, ref-all, first, lastrest 等.. 许多列表操作函数也能操作字符串(newlisp 的列表和字符串操作函数可以说是所有lisp方言里最方便快速的了).

还有一点,索引可以是负数,这时候就会从字符串或者列表的最右端开始检索我们需要的数据:

(set 'L '(a b c d))
(L -1)   → d
(L -2)   → c
(-3 2 L) → (b c)
   
(set 'S  "abcd")
   
(S -1)   → d
(S -2)   → c
(-3 2 S) → "bc")

pushpop

像列表添加元素用 push, 消除一个元素用 pop. 两个函数都是破坏性的,会改变列表的内容:

(set 'L '(b c d e f))
   
(push 'a L) → (a b c d e f)
(push 'g L -1) ; 将 'g 压入 列表的最后
(pop L)        ; pop 第一个数据 a
(pop L -1)     ; pop 最后的数据 g
(pop L -2)     ; pop 倒数第二个数据 e
(pop L 1)      ; pop 第二个数据 c
   
L → (b d f)
   
; 多维 push / pop
(set 'L '(a b (c d (e f) g) h i))
   
(push 'x L 2 1) → (a b (c x d (e f) g) h i)
   
L → (a b (c x d (e f) g) h i)
   
(pop L 2 1) → x

经过newLISP的优化,push到末端和push到开始效率是一样的(一样的一样的啊).

用索引向量 V push 进嵌套列表的数据,也可以同样通过pop V 来获得:

(set 'L '(a b (c d (e f) g) h i))
(set 'V '(2 1))
(push 'x L V)
L → (a b (c x d (e f) g) h i))
(ref 'x L) → (2 1) ; search for a nested member
(pop L V) → 'x

extend 扩展列表

extend 也能破坏性的改变列表. 和 push pop 一样, extend 要修改的列表作为第一个参数.

(set 'L '(a b c))
(extend L '(d e) '(f g))

L → '(a b c d e f g)

; extending in a place

(set 'L '(a b "CD" (e f)))
(extend (L 3) '(g))
L → (a b "CD" (e f g))

访问列表元素

多位索引能够用来访问嵌套列表内的指定元素:

(set 'L '(a b (c d (e f) g) h i))
   
; old syntax only for one index
(nth 2 L) → (c d (e f) g)
   
; use new syntax for multiple indices
(nth '(2 2 1) L) → f
(nth '(2 2) L) → (e f)
   
; vector indexing
(set 'vec '(2 2))
(nth vec L) → (e f)
   
; 隐式索引
(L 2 2 1) → f
(L 2 2)   → (e f)
   
; 隐式索引配合索引向量
(set 'vec '(2 2 1))
(L vec)   → f

最后一个例子中的隐式索引提高了代码的可读性. 用一个索引列表获取一个列表的部分数据, 一切都是索引(Oh Year 哈利路亚亚~~~~~).

隐式索引同样也是适用于 rsst 和 slice:

(rest '(a b c d e))      → (b c d e)
(rest (rest '(a b c d e) → (c d e)
; same as
(1 '(a b c d e)) → (b c d e)
(2 '(a b c d e)) → (c d e)
   
; negative indices
(-2 '(a b c d e)) → (d e)
   
; slicing
(2 2 '(a b c d e f g))  → (c d)
(-5 3 '(a b c d e f g)) → (c d e)

获取多个列表元素

有时候要从列表里获取多个元素,这时候可以用 select:

; pick several elements from a list
(set 'L '(a b c d e f g))
(select L 1 2 4 -1) → (b c e g)
   
; indices can be delivered in an index vector:
(set 'vec '(1 2 4 -1))
(select L vec) → (b c e g)

用 select 可以很方便的在同一时间,实现重排列表或者双倍化列表:

(select L 2 2 1 1) → (c c b b)

过滤和差分列表

用filter可以过滤列表,访问符合条件的数据:

(filter (fn(x) (< 5 x)) '(1 6 3 7 8))    → (6 7 8)
(filter symbol? '(a b 3 c 4 "hello" g)) → (a b c g)
(difference '(1 3 2 5 5 7) '(3 7)) → (1 2 5)

第一个例子可以写的更简洁, 如下:

(filter (curry < 5) '(1 6 3 7 8))

The curry 自己提供了一个参数 5 给函数 < .然后组装出了一个新的函数给 filter:

(curry < 5) → (lambda (_x) (< 5 _x))

通过 curry, 可以快速的将两个参数的函数变成一个参数的函数(其中一个预先设置好了).

修改列表元素

setf 可以改变由 nth 或者 assoc 所引用的列表元素(set 只能改变symbol的,而setf就是用来弥补这个不足的):

; modify a list at an index
(set 'L '(a b (c d (e f) g) h i))
   
(setf (L 2 2 1) 'x) → x   
L → (a b (c d (e x) g) h i)
(setf (L 2 2) 'z) → z
L → (a b (c d z g) h i)
   
; modify an association list
(set 'A '((a 1) (b 2) (c 3)))
   
; using setf with assoc
(setf (assoc 'b A) '(b 22)) → (b 22)
A → ((a 1) (b 22) (c 3))
; using setf with lookup
(setf (lookup 'c A) 33) → 33
A → ((a 1) (b 22) (c 33))

anaphoric 变量

newLISP内部的 anaphoric 系统变量 $it 保留了旧的列表元素. 用这个变量可以很方便的将旧元素变成相关的新元素:

(set 'L '(0 0 0))
(setf (L 1) (+ $it 1)) → 1 ; the new value
(setf (L 1) (+ $it 1)) → 2
(setf (L 1) (+ $it 1)) → 4
L → '(0 3 0)

下面的这些函数都是用了 anaphoric 变量 $it: find-all, replace, set-ref, set-ref-allsetf setq.

普通列表的替换操作

列表和字符串都可以替换,一次可以替换一个元素也可以替换多个. 结合 matchunify 还能组合出更复杂的判断条件. 和 setf 一样, 替换表达式也能够使用$it获取旧的元素值.

(set 'aList '(a b c d e a b c d))
     
(replace 'b aList 'B) → (a B c d e a B c d)

函数 replace 能够使用一个判断函数,提取出自己需要的数据:

; replace all numbers where 10 < number
(set 'L '(1 4 22 5 6 89 2 3 24))
     
(replace 10 L 10 <) → (1 4 10 5 6 10 2 3 10)

用内建的 matchunify 能够定义更复杂的条件:

; replace only sublists starting with 'mary'
    
(set 'AL '((john 5 6 4) (mary 3 4 7) (bob 4 2 7 9) (jane 3)))
   
(replace '(mary *)  AL (list 'mary (apply + (rest $it))) match)
→ ((john 5 6 4) (mary 14) (bob 4 2 7 9) (jane 3))
    
; make sum in all expressions
    
(set 'AL '((john 5 6 4) (mary 3 4 7) (bob 4 2 7 9) (jane 3)))
   
(replace '(*) AL (list ($it 0) (apply + (rest $it))) match)
→ ((john 15) (mary 14) (bob 22) (jane 3))
    
$0 → 4  ; 替换成功的个数
    
; 只改变拥有相同元素的子列表
    
(replace '(X X) '((3 10) (2 5) (4 4) (6 7) (8 8)) (list ($it 0) 'double ($it 1)) unify)
→ ((3 10) (2 5) (4 double 4) (6 7) (8 double 8))
    
$0 → 2  ; replacements made

在替换的过程中 $0$it 都包含了已经符合条件的元素(也就是说上面的$it 换成 $0是一样的,十万个为什么--!请爬手册).

替换成功后newLISP设置 $0 为替换成功的个数.

嵌套列表的替换操作

很多时候列表都是嵌套的, 比如分析XML得到的 SXML 数据. 函数 ref-set(ver10 已抛弃), set-ref and set-ref-all 能够用来寻找嵌套列表中的单个或者多个元素, 并替换他们.

(set 'data '((monday (apples 20 30) (oranges 2 4 9)) (tuesday (apples 5) (oranges 32 1))))
   
(set-ref 'monday data tuesday)
→ ((tuesday (apples 20 30) (oranges 2 4 9)) (tuesday (apples 5) (oranges 32 1))) 

函数 set-ref-all 就行多次的 set-ref 操作, 替换所有符合的列表元素.

(set 'data '((monday (apples 20 30) (oranges 2 4 9)) (tuesday (apples 5) (oranges 32 1))))
   
(set-ref-all 'apples data "Apples")
→ ((monday ("Apples" 20 30) (oranges 2 4 9)) (tuesday ("Apples" 5) (oranges 32 1)))

find, replace, ref and ref-all 一样, 可以用 match 或者 unify组合出更复杂的判别表达式:

(set 'data '((monday (apples 20 30) (oranges 2 4 9)) (tuesday (apples 5) (oranges 32 1))))
   
(set-ref-all '(oranges *) data (list (first $0) (apply + (rest $it))) match)
→ ((monday (apples 20 30) (oranges 15)) (tuesday (apples 5) (oranges 33)))

最后一个例子中用 $0 获取符合条件的旧元素并用之生成新元素. 最后所有 oranges 的记录都被做了求和运算. 这里$it$0 是通用的.

通过引用传递列表

有时候我们要传递一个非常大的列表 (超过100个元素) 给用户函数修改 . 通常 newLISP 会将所有的元素拷贝一份给函数(值传递的开销是很大的). 下面的代码展示了一种非常实用的技巧.通过引用来传递大量的数据:

(set 'data:data '(a b c d e f g h))
   
(define (change db i value)
    (setf (db i) value))
   
(change data 3 999) → d
   
data:data → '(a b c 999 d e f g h)

在这个例子所有数据都被塞进了 data 这个context里,同时将它赋值给自己的默认函数 data .

当一个函数需要列表或者字符串的时候, 将这个 context 传递给函数, context就会自动映射到自己的默认函数上.

很多内建函数可以返回一个函数的引用, 这在修改列表的时候很有用:

(set 'L '(r w j s r b))
   
(pop (sort L)) → b
   
L → (j r r s w)

变量的扩展

有两个函数可以做宏扩展: expand and letex (掌握这两个宏就不怕了,说白了就是列表). 函数 expand 有三种不同的语法模式.

Symbols 被扩展成他们的值:

; expand from one or more listed symbols
(set 'x 2 'a '(d e))
(expand '(a x (b c x)) 'x 'a)  → ((d e) 2 (b c 2))

在编写 lambda 表达式,扩展函数(或者宏)内的变量的时候, expand 变得非常有用 (fexpr with define-macro):

; use expansion inside a function
(define (raise-to power)
    (expand (fn (base) (pow base power)) 'power))
(define square (raise-to 2))
(define cube (raise-to 3))
(square 5)  → 25
(cube 5)    → 125

expand 可以带一个关联列表:

; expand from an association list
(expand '(a b c) '((a 1) (b 2)))                → (1 2 c)
(expand '(a b c) '((a 1) (b 2) (c (x y z))))    → (1 2 (x y z))

关联列表里的表达式也可以先运算:

; evaluate the value parts in the association list before expansion
(expand '(a b) '((a (+ 1 2)) (b (+ 3 4))))      → ((+ 1 2) (+ 3 4))
(expand '(a b) '((a (+ 1 2)) (b (+ 3 4))) true) → (3 7)

expand 的最后一种语法就是用大写字母开头的变量,这时候的扩展变量可以先不指定.

; expand from uppercase variables
(set 'A 1 'Bvar 2 'C nil 'd 5 'e 6)
(expand '(A (Bvar) C d e f))  → (1 (2) C d e f)

用这个方法.之前的函数定义可以写的更简洁.

; use expansion from uppercase variables in function factories
(define (raise-to Power) 
    (expand (fn (base) (pow base Power))))
(define cube (raise-to 3)) → (lambda (base) (pow base 3))
(cube 4) → 64

letex 函数和 expand 类似, 不过扩展的 symbols 是 letex 局部申明的(个人经验在写比较复杂的macro时letex比较给力).

; use letex for variable expansion
(letex ( (x 1) (y '(a b c)) (z "hello") ) '(x y z)) → (1 (a b c) "hello")

注意在 letex 的主体表达式: (x y z) 之前的括号是为了防止运算(否则会报告函数x不存在).

解构嵌套列表

下面的方法可以将嵌套列表内部的元素绑定到指定变量上:

; uses unify together with bind for destructuring
(set 'structure '((one "two") 3 (four (x y z))))
(set 'pattern '((A B) C (D E)))
(bind (unify pattern structure))
A → one
B → "two"
C → 3
D → four
E → (x y z)

§

7. 程序流

在 newLISP 里程序流是最实用的, 除了拥有循环和分支以外,还有专门的 catchthrow 用来打破这些常规的流.

循环表达式和其他的函数或者代码块一样,返回最后一个表达式的值.

循环

newLISP支持大部分传统的循环模式. 循环内变量,遵循 dynamic scoping (动态作用域):

; loop a number of times
; i goes from 0 to N - 1
(dotimes (i N)
    ....
)
   
; demonstrate locality of i
(dotimes (i 3)
    (print i ":")
    (dotimes (i 3) (print i))
    (println)
)
   
→ ; will output
 0:012
 1:012
 2:012
   
; loop through a list
; takes the value of each element in aList
(dolist (e aList)
    ...
)
   
; loop through a string
; takes the ASCII or UTF-8 value of each character in aString
(dostring (e aString)
    ...
)
   
; loop through the symbols of a context in
; alphabetical order of the symbol name
(dotree (s CTX)
    ...
)
   
; loop from to with optional step size
; i goes from init to <= N inclusive with step size step
; Note that the sign in step is irrelevant, N can be greater
; or less then init.
(for (i init N step)
    ...
)
   
; loop while a condition is true
; first test condition then perform body
(while condition
    ...
)
   
; loop while a condition is false
; first test condition then perform body
(until condition
    ...
)
   
; loop while a condition is true
; first perform body then test
; body is performed at least once
(do-while condition
    ...
)
   
; loop while a condition is false
; first perform body then test
; body is performed at least once
(do-until condition
    ...
)(

dolist, dotimes and for 可以添加一个中断条件. 当中断条件达成时,循环提前结束:

(dolist (x '(a b c d e f g) (= x 'e))
    (print x))
→ ; will output
 abcd

代码块

代码块就是一些需要相续执行的代码的集合. 所有循环表达式的主体(body)都是代码块(不过不用我们显示申明).

用begin可以显示的申明代码块:

(begin
    s-exp1
    s-exp2
     ...
    s-expN)

begin 一般和 if and cond 配合起来使用(if 一个条件只能执行一条语句,要执行多条,必须用代码块).

and, or, let, letn and local 也能用来构造代码块.

分支

(if condition true-expr false-expr)
   
;or when no false clause is present
(if condition true-expr)
   
;or unary if for (filter if '(...))
(if condition)
   
; more than one statement in the true or false
; part must be blocked with (begin ...)
(if (= x Y)
    (begin
        (some-func x)
        (some-func y))
    (begin
        (do-this x y)
        (do-that x y))
)
 
; the when form can take several statements without
; using a (begin ...) block
(when condition
    exp-1
    exp-2
    ...
)
   
; if-not works like (if (not ...) ...)
(if-not true-expr false-expr)
   
; unless works like (when (not ...) ...)
(unless condition
    exp-1
    exp-2
    ...
)

condition 计算为真执行 exp-true 计算为假则执行 exp-false .

if 表达式里可以成对的使用 condition/exp-true , 这看起来就像 cond (不过是少了括号的cond GaGa~_~):

(if condition-1 exp-true-1
    condition-2 exp-true-2
    ...
    condition-n exp-true-n
    expr-false
)

只要 condition-i 计算的值为真 exp-true-i 就计算然后返回, 如果没有一个条件符合就会返回 exp-false .

cond 像多个判断条件的if语句,但是每个 condition-i exp-true-i 外面都必须加上括号:

(cond
    (condition-1 exp-true-1 )
    (condition-2 exp-true-2 )
                ...
    (condition-n exp-true-n )
    (true exp-true)
)

混沌流

使用 amb 能够产生一个随机的流:

(amb
    exp-1
    exp-2
    ...
    exp-n
)

exp-1exp-n 每一个表达式被计算并返回的可能性是 p = 1/n .

catchthrow

catch 表达式能够包含任何的循环或者代码块. 如果 throw 表达式被执行,则整个catch就会立刻返回,返回值就是throw出来的数据.

(catch
    (dotimes (i 10)
    (if (= i 5) (throw "The End"))
    (print i " "))
)
; will output
0 1 2 3 4
; and the return value of the catch expression will be
→ "The End"

多个 catch 表达式可以嵌套起来使用. 同时 catch 也能捕捉错误. 具体细节请看下章分解 //Error Handling// .

使用中断条件离开循环

使用 dotimes, dolist or for 创建的循环都能够通过添加中断条件,提前离开循环:

(dotimes (x 10 (> (* x x) 9))
    (println x))
→
 0
 1
 2
 3
   
(dolist (i '(a b c nil d e) (not i))
    (println i))
→
 a
 b
 c

andor 改变程序流

和 Prolog language 类似, 逻辑操作符 andor 依靠各个表达式的计算值完成整个流的逻辑控制:

 
(and
   expr-1
   expr-2
    ...
   expr-n)

and 表达式顺序执行各个expr,直到其中一个 expr-i 计算的值为 nil或者空列表() 或者所有的 expr-i 都计算完才返回. 最后一个计算的表达式就是整个 and 代码块的返回值(不是最后一个表达式而是最后一个计算的!!!).

(or
   expr-1
   expr-2
    ...
   expr-n)

or 表达式顺序执行各个expr,直到其中一个 expr-i 计算的值既不为 nil 也不为空列表() 或者所有的 expr-i 都计算完才返回 . 最后一个计算的表达式就是整个 or 代码块的返回值.

§

8. 错误处理

在newLISP中有也会产生错误信息. 完整的错误列表,请看 newLISP 手册附录.

newLISP 错误

下面几种情况会产生错误: 用错误的语法调用函数, 传递给函数的参数数量错误,参数数据不符合要求, 或者试着调用不存在的函数.

; examples of newLISP errors
;
(foo foo)   → invalid function : (foo foo)
(+ "hello") → value expected in function + : "hello"

用户定义错误

用户定义的错误用 throw-error 抛出:

; user defined error
;
(define (double x)
    (if (= x 99) (throw-error "illegal number"))
    (+ x x)
)
   
(double 8)   → 16
(double 10)  → 20
(double 99)
→
user error : illegal number
called from user defined function double

错误事件处理函数

函数 error-event 可以捕捉所有的错误事件(用户的和newLISP的),然后调用我们自定义的函数进行处理.

; define an error event handler
;
(define (MyHandler)
    (println  (last (last-error))  " has occurred"))
   
(error-event 'MyHandler)
   
(foo) → ERR: invalid function : (foo) has occurred 

捕捉错误

用 catch 可以手动的捕捉错误事件,这样就可以更细微的控制代码.

(define (double x)
    (if (= x 99) (throw-error "illegal number"))
    (+ x x))

可以给 catch 添加第二个参数,用来存储错误信息:

(catch (double 8) 'result) → true
result → 16
(catch (double 99) 'result) → nil
(print result)
 →
user error : illegal number
called from user defined function double
    
(catch (double "hi") 'result) → nil
(print result)
→
value expected in function + : x
called from user defined function double

在没有错误产生的情况下,整个 catch 表达式返回 true , 表达式的计算结果会存放在 result 里.

如果一个错误异常发生, catch 将会捕捉到错误,并将错误信息存放在 result 里,然后整个表达式返回 nil.

操作系统错误

有些信息是操作系统层的, newLISP 无法捕捉, 不过可以用 sys-error 获取. 比如打开一个不存在的文件:

; trying to open a nonexistent file
(open "blahbla" "r")  →  nil
(sys-error)           →  (2 "No such file or directory")
   
   
; to clear errno specify 0
(sys-error 0)         →  (0 "Unknown error: 0")

不同的 Unix 系统,产生的错误信息可能不一样的. 具体代码可查看 /usr/include/sys/errno.h .


§

9. 函数即数据

任意拆装的函数

(define (double x) (+ x x))
→ (lambda (x) (+ x x))
    
(first double) → (x)
(last double)  → (+ x x)
    
; make a fuzzy double
(setf (nth 1 double) '(mul (normal x (div x 10)) 2))
    
(double 10) → 20.31445313
(double 10) → 19.60351563

newLISP 的lambda 既不是一个操作符,也不是一个 symbol, 但是他同时拥有了表达式 (s-expression) 和列表的属性:

(first double) → (x)   ; not lambda

lambda 可以用 append 对进行右结合:

(append (lambda) '((x) (+ x x))) → (lambda (x) (+ x x))
; or shorter
(append (fn) '((x) (+ x x))) → (lambda (x) (+ x x))
    
(set 'double (append (lambda) '((x) (+ x x)))
    
(double 10) → 20

也可以使用 cons 进行左结合:

(cons '(x) (lambda) → (lambda (x))

Lambda expressions in newLISP never lose their first class object property.

lambda 可以简写成 fn, 在 mapping 或者 applying 函数的时候可以缩短代码长度,同时提可读性.

Mapping 和 applying

map 函数可以将一个列表的数据依次传递给指定的函数或者操作符,最后每个元素计算后的结果再以列表的形式集体返回.

(define (double (x) (+ x x))
     
(map double '(1 2 3 4 5)) → (2 4 6 8 10)

apply 函数则是将列表中所有的数据作为参数一次性全部(传递的个数也是可以控制的,具体爬手册)传递给指定和函数或者操作符,然后得到一个结果:

(apply + (sequence 1 10)) → 55

函数制造函数

这是一个将表达式当做参数传递的例子:

; macro expansion using expand
(define (raise-to power)
    (expand (fn (base) (pow base power)) 'power))
     
; or as an alternative using letex
(define (raise-to power)
    (letex (p power) (fn (base) (pow base p))))
     
(define square (raise-to 2))
     
(define cube (raise-to 3))
     
(square 5)   → 25
(cube 5)     → 125

内建函数 curry 可以为函数(拥有两个参数)预设一个值,进而生成一个新的只需要一个参数的函数.

(define add-one (curry add 1))  → (lambda () (add 1 ($args 0)))
     
(define by-ten (curry mul 10))  → (lambda () (mul 10 ($args 0)))
   
(add-one 5)    → 6
     
(by-ten 1.23)  → 12.3

记住预设的参数必须是原函数的第一个参数(当然可以通过reader-event 添加自己语法系统).

用函数存储状态

可以创建一个专门的 context 来存储状态:

; newLISP generator

(define (gen:gen)
    (setq gen:sum 
    (if gen:sum (inc gen:sum) 1)))

; this could be written even shorter, because
; 'inc' treats nil as zero

(define (gen:gen)
    (inc gen:sum))

(gen) → 1
(gen) → 2 
(gen) → 3

由于使用了默认函数,所以使用 context 名就会自动调用默认的生成函数. 别的函数也可以加入 context , 比如初始化函数.

(define (gen:init x)
    (setq gen:sum x))

(gen:init 20) → 20

(gen) → 21
(gen) → 22

函数的自我修改

The first class nature of lambda expressions in newLISP makes it possible to write self modifying code:

;; sum accumulator
(define (sum (x 0)) (inc 0 x))

(sum 1)    → 1
(sum 2)    → 3
(sum 100)  → 103
(sum)      → 103

sum  → (lambda ((x 0)) (inc 103 x))

下面的制造函数使用 expand 创建了一个能够自我修改的流函数:

(define (make-stream lst)
    (letex (stream lst) 
        (lambda () (pop 'stream))))

(set 'lst '(a b c d e f g h))
(define mystream (make-stream lst))

(mystream)  → a
(mystream)  → b
(mystream)  → c

因为 pop 也可以处理字符串,所以创造的函数也可以处理字符流:

(set 'str "abcddefgh")
(define mystream (make-stream str))

(mystream)  → "a"
(mystream)  → "c"

§

10. 文本处理

正则表达式

newLISP 里有一大堆的函数能使用正则表达式:

function function description
directory Return a list of files whose names match a pattern.
ends-with Test if a string ends with a key string or pattern.
find Find the position of a pattern.
find-all Assemble a list of all patterns found.
parse Break a string into tokens at patterns found between tokens.
regex Find patterns and returns a list of all sub patterns found, with offset and length.
replace Replace found patterns with a user defined function, which can take as input the patterns themselves.
search Search for a pattern in a file.
starts-with Test if a string starts with a key string or pattern.

函数 find, regex, replace and search 将找到的符合匹配模式的数据保存在系统变量 $0 到 $15. 具体细节请撕手册(谢谢).

The following paragraphs show frequently-used algorithms for scanning and tokenizing text.

扫描文本

函数 replace 配合正则表达式能够用来扫描文本. 匹配模式(第二个参数)定义了需要扫描出来的文本标记. 每一个被扫描出来的标记都push进一个列表,具体操作由 replace 的第四个参数完成. 下面的代码将指定网页中的所有lsp文件写入本地磁盘:

#!/usr/bin/newlisp

; tokenize using replace with regular expressions
; names are of the form <a href="example.lsp">example.lsp</a>
   
(set 'page (get-url "http://newlisp.digidep.net/scripts/"))
(replace {>(.*lsp)<} page (first (push $1 links)) 0) ; old technique
;(set 'links (find-all {>(.*lsp)<} page $1)) ; new technique
  
(dolist (fname links)
   (write-file fname (get-url (append "http://newlisp.digidep.net/scripts/" fname)))
   (println "->" fname))

(exit)

花括号 ({,}) 用来转义那些在正则表达式中拥有特殊意义的符号比如 (") .

另一种方法可以让代码更短. 函数 find-all 将所有匹配的字符串push进一个列表:

(set 'links (find-all {>(.*lsp)<} page $1)) ; new technique

find-all 也可以直接对查找出来的每一个匹配文本进行操作(个人建议只写短代码,否则你会非常疼,以后看的更疼):

(find-all {(new)(lisp)} "newLISPisNEWLISP" (append $2 $1) 1)
→ ("LISPnew" "LISPNEW")

这个例子里 find-all 将所有找到的符合条件的字符串两两颠倒后输出(最后的1表示正则表达式忽略大小写,默认是0).

了一个标记文本的例子我们用 parse. 和 replace and find-all 不同, parse 不是定义需要的标记而是定义标记之间的内容,使之能够将文本提取出来:

; tokenize using parse
(set 'str "1 2,3,4 5, 6 7  8")
(parse str {,\ *|\ +,*} 0)
→ ("1" "2" "3" "4" "5" "6" "7" "8")

如果没有使用花括号而是使用双引号("), 反斜杠就要写两个. 记住有一个空格在每一个反斜杠后面.

组合字符串

append 和 join 可以用组合新的字符串:

(set 'lstr (map string (rand 1000 100)))
→ ("976" "329" ... "692" "425")
   
; the wrong slowest way
(set 'bigStr "")
(dolist (s lstr)
    (set 'bigStr (append bigStr s)))
   
; smarter way - 50 times faster
;
(apply append lstr)

上面组装出来的字符串并不如在原列表里看的那么清晰. 当然我们可以在append之前修改每一个元素. 不过更好的方法是用 join ,并给他提供一个额外的间隔参数:

; smartest way - 300 times faster
; join an existing list of strings
;
(join lstr) → "97632936869242555543 ...."
   
; join can specify a string between the elements
; to be joined
(join lstr "-") → "976-329-368-692-425-555-43 ...."

扩展字符串

很多时候在特定的位置扩展字符串变得非常必要.函数 extend 用于在一个字符串末尾添加字符串(这个函数是破坏性的,append不是,apeend需要扩展的字符串必须已经存在,而他不用). push 可以在字符串的任意位置扩展新的字符串.

.
; smartest way - much faster on big strings
; grow a string in place

; using extend
(set 'str "")
(extend str "AB" "CD")
str → "ABCD"

; extending in a place
(set 'L '(a b "CD" (e f)))
(extend (L 2) "E")
L → (a b "CDE" (e f))

; using push
(set 'str "")
(push "AB" str -1)
(push "CD" str -1)
str → "ABCD"

重排字符串

select 可以提取列表元素,也可以提取字符串中的字符,并重排成新的字符串:

(set 'str "eilnpsw")
(select str '(3 0 -1 2 1 -2 -3)) → "newlisp"
   
; alternative syntax
(select str 3 0 -1 2 1 -2 -3) → "newlisp"

第二个例子非常实用,因为在编码的时候一般都用变量存储索引.

修改字符串

在 newLISP 里有各种各样的函数可以破坏性的修改字符串:

function description
extend Extend a string with another string.
push pop Insert or extract one or more characters at a specific position.
replace Replace all occurrences of a string or string pattern with a string.
setf Replace a character in a string with one or more characters.

replace 也可以删除任何匹配的子字符串(用 "" 替换).

在 UTF-8 版本的 newLISP 里无论是 nth 还是隐式索引, 都会根据字符编码的不同而自动改变操作长度(因为一个 UTF-8 字符不止一个字节).


§

11. 字典 和 hashes

Hash-like 键 → 值 访问

众所周知 sym (这个一定要爬手册非常重要)可以创建和管理symbol, context 可以创建命名空间. 在newLISP的远古时代, 结合sym 和 context 就可以创建 hash-like 的数据结构并且通过键名得到相应的键值. 现在我们有了更方便的方法,直接使用未初始化 默认函数 的命名空间:

(define Myhash:Myhash) ; establish the namespace and default functor

另一种方法就是使用 Tree (想知道这个空间有什么,在MAIN里执行(println (source)) 看看)这个系统预先创建的context,重新实例化一个我们自己的context:

(new Tree 'Myhash)

两种方法的作用是一样的.

default functor (默认函数)就是和自己的空间名 (context) 同名的函数. 如果这个函数只包含 nil 的话, 他就可以像 hash 函数一样工作:

(Myhash "var" 123) ; create and set variable/value pair
 
(Myhash "var") → 123 ; retrieve value
 
(Myhash "foo" "hello")
 
(Myhash "bar" '(q w e r t y))
 
(Myhash "!*@$" '(a b c))

将指定的symbol设置成 nil 就可以擦除这个数据:

(Myhash "bar" nil)

为了防止键名和 newLISP 内部的symbol冲突, 每一个键名的全面都加上了 (_). 键值可以是任何字符串,数字,以及表达式.

Myhash 可以转换成关联列表:

(Myhash) → (("!*@$" (a b c)) ("foo" "hello") ("var" 123))

symbols 函数可以查看 Myhash 空间内所有的symbol:

(symbols Myhash) → (Myhash:Myhash Myhash:_!*@$ Myhash:_foo Myhash:_var)

也可以通过已存在的关联列表创建字典:

(set 'aList '(("one" 1) ("two" 2) ("three")))
 
(Myhash aList)
 
(Myhash) → (("!*@$" (a b c)) ("foo" "hello") ("one" 1) ("three" nil) ("two" 2) ("var" 123))

保存和加载字典

使用save 就可以轻易的将 Myhash 内的所有数据序列化成一句句的语句保存在指定的文件内:

(save "Myhash.lsp" 'Myhash)

整个命名空间内的数据都保存在了 Myhash.lsp 文件里,加载起来也非常简单:

(load "Myhash")

注意:hash 创建的命名空间和 bayes-train 创建的很像. 所有的键名字符串都加上了前置的下划线,然后转换成symbol. 这就意味着可以像操作hash一样操作 bayes-train 创建的空间,从而获取各个键值和统计数据. 具体细节请看手册中的 bayes-train 函数介绍.


§

12. TCP/IP 客户端 服务器

Open connection

这种模式下服务器不断接受客户端发送的信息,直到客户端主动关闭连接, 然后服务器在继续循环 net-accept 等待新的客户端连接:

; sender listens
(constant 'max-bytes 1024)
(if (not (set 'listen (net-listen 123)))
    (print (net-error)))
(while (not (net-error))
    (set 'connection (net-accept listen)) ; blocking here
    (while (not (net-error))
         (net-receive connection message-from-client max-bytes)
         .... process message from client ...
         .... configure message to client ...
         (net-send connection message-to-client)) 
)

客户端:

; client connects to sender
(if (not (set 'connection (net-connect "host.com" 123)))
    (println (net-error)))
; maximum bytes to receive
(constant 'max-bytes 1024)
; message send-receive loop
(while (not (net-error))
     .... configure message to server ...
     (net-send connection message-to-server)
     (net-receive connection message-from-server max-bytes)
     .... process message-from-server ...
)

Closed transaction

这种模式下服务器只要接收到一条信息,就关闭客户端连接,然后重新等待下一次连接.

; sender
(while (not (net-error))
    (set 'connection (net-accept listen)) ; blocking here
    (net-receive connection message-from-client max-bytes)
        .... process message from client ...
        .... configure message to client ...
    (net-send connection message-to-client)
    (close connection)
)

客户端每发送一次信息就要重新连接服务器才能再发送信息:

 
; client
(unless (set 'connection (net-connect "host.com" 123))
    (println (net-error))
    (exit))
; maximum bytes to receive
(constant 'max-bytes 1024)
  .... configure message to server ...
(net-send connection message-to-server)
(net-receive connection message-from-server max-bytes)
  .... process message-from-server ...

更多的建立 client/server 方法, 请查看newLISP手册.


§

13. UDP 通信

UDP 比 TCP/IP 更快更简单,同时也提供 multi casting (多播通信). 但是因为缺少数据校验,相对来说也更不可靠, (例如.根本没有包序列). 不过如果只在本地局域网或者小范围的机器之间通信,UDP还是不错的.

Open connection

这个例子中服务器一直打开连接进行通信. 通信的过程中时候使用了 net-listen, net-receive-from and net-send-to.

无论是客户端还是服务器 net-listen 都需要加上 "udp" 选项(否则将进行TCP通信). net-listen 只负责绑定socket到本地, 不能够用来监听连接(因为UDP没有握手协议).net-receive-from 负责接受客户端信息(会一直阻塞到接收到信息). net-send-to 从接收到的信息中分解出客户端地址(nth 1 msg),发送信息给客户端.

发送:

; sender
(set 'socket (net-listen 10001 "" "udp"))
(if socket (println "server listening on port " 10001)
   (println (net-error)))
(while (not (net-error))
   (set 'msg (net-receive-from socket 255))
   (println "->" msg)
   (net-send-to (nth 1 msg) (nth 2 msg)
        (upper-case (first msg)) socket)
)

客户端:

(set 'socket (net-listen 10002 "" "udp"))
(if (not socket) (println (net-error)))
(while (not (net-error))
    (print "->")
    (net-send-to "127.0.0.1" 10001 (read-line) socket)
    (net-receive socket buff 255)
    (println "→" buff)
)

Closed transaction

这种模式一般用来控制硬件或者设备. 不需要提前准备直接就能收发信息, 一个函数发送, 另一个函数接收.

; wait for data gram with maximum 20 bytes
(net-receive-udp 1001 20)
; or
(net-receive-udp 1001 20 5000000)  ; wait for max 5 seconds
; the sender
(net-send-udp "host.com" 1001 "Hello")

Win32 和 Unix's 在接收或者发送数据多于或少于指定数值时,给出的反馈信息是不一样的.

多播通信

这种模式下服务端使用 net-listen 函数注册一个多播地址.

; example server
(net-listen 4096 "226.0.0.1" "multi") → 5
(net-receive-from 5 20)
 
; example client I
(net-connect "226.0.0.1" 4096 "multi") → 3
(net-send 3 "hello")
; example client II
(net-connect "" 4096 "multi") → 3
(net-send-to "226.0.0.1" 4096 "hello" 3)

例子中的 net-receive 链接是阻塞的,不过可以配合 net-select 或者 net-peek 实现非阻塞通信.


§

14. 非阻塞通信

使用 net-select

前面的例子中,所有的客户端在接受信息的时候都是阻塞的.函数 net-select 可以实现非阻塞通信:

 
; 每100ms毫秒检查一次是否有客户端链接
(while (not (net-select connection "r" 100000))
    (do-something-while-waiting ...))
 
(net-receive...)

connection 可以是一个socket也可以是一个列表的socket(net-select能够同时检查多个socket).

使用 net-peek

net-peek 返回有多少字符可以读取(这个函数是直接返回的,如果一直检测CPU会很高).

(while ( = (net-peek aSock) 0)
    (do-something-while-waiting ...))
(net-receive...)

§

15. 操作其他程序

使用 exec

这种方法只适用于简单的执行程序,然后获得输出(无法交互而且是阻塞的).

(exec "ls *.c") → ("." ".." "util.c" "example.ls")

exec 首先打开一个进程管道,接着调用 Unix 命令行工具 ls 打印出需要的文件名,继而将本应输出到 STDOUT 的内容,一行行的存储到一个列表中.

这里我们使用另一个函数 process 来启动新程序. 这个函数在启动程序后会立刻返回而不会阻塞.

接下来的三个样例里的服务器都不是独立的,他们由客户端控制启动然后通过不同的协议通信:

     → launch server
     → talk to server
     ← wait for response from server
     → talk to server
     ← wait for response from server
          ...

有时候客户端需要等待一段时间(很短),以便让服务器启动好. 除了第一个例子是使用wish,其他的两个全是GTK-Server (这个东西还是不错滴)的代码片段[http://www.gtk-server.org www.gtk-server.org]. 基本的编程逻辑和别的程序一样.

标准 I/O 管道

函数 process 允许提供两个管道给应用程序,以便重定向输入和输出.

; setup communications
(map set '(myin tcout) (pipe))
(map set '(tcin myout) (pipe))
(process "/usr/bin/wish" tcin tcout)
 
; make GUI
(write myout
[text]
wm geometry . 250x90
wm title . "Tcl/Tk and newLISP"
bind . <Destroy> {puts {(exit)}}
[/text])
 
; run event loop
(while (read-line myin)
    (eval-string (current-line))
)

代码中使用了双向管道,使得程序之间可以交互(windows下的wish路径一般是"C:/Tcl/bin/wish.exe"). 比只使用一个命令的 exec 函数能做更多的事情.

更详细的 Tcl/Tk 例子请自行下载源码版本的newLISP查看: examples/tcltk.lsp (从ver10开始,发布了新的GUI-SERVER,不过老式的tk是一样可用的).

使用 TCP/IP 通信

; Define communication function
(define (gtk str , tmp)
    (net-send connection str)
    (net-receive connection tmp 64)
    tmp)
 
; Start the gtk-server
(process "gtk-server tcp localhost:50000")
(sleep 1000)
 
; Connect to the GTK-server
(set 'connection (net-connect "localhost" 50000))
(set 'result (gtk "gtk_init NULL NULL"))
(set 'result (gtk "gtk_window_new 0"))
               .....

使用命名的 FIFO通信

首先创建一个 FIFO (看起来像一个文件,更多代码看GTK-SERVER目录下的demo文件夹):

(exec "mkfifo myfifo")
 
; or alternatively
 
(import "/lib/libc.so.6" "mkfifo")
(mkfifo "/tmp/myfifo" 0777)
 
; Define communication function
(define (gtk str)
	(set 'handle (open "myfifo" "write"))
	(write handle str)
	(close handle)
	(set 'handle (open "myfifo" "read"))
	(read handle tmp 20)
	(close handle)
tmp)

使用 UDP 通信

注意监听函数提供的 "udp" 选项,只是用来绑定socket的,并不能像TCP那样用来接收连接.

; Define communication function
(define (gtk str , tmp)
(net-send-to "localhost" 50000 str socket)
(net-receive socket 'tmp net-buffer)
tmp)
 
; Start the gtk-server
(define (start)
	(process "gtk-server udp localhost:50000")
	(sleep 500)
	(set 'socket (net-listen 50001 "localhost" "udp")) )
 
(set 'result (gtk "gtk_init NULL NULL"))
 
(set 'result (gtk "gtk_window_new 0"))
.....

§

16. 阻塞式执行程序

执行shell命令

这个命令在 newLISP's 交互式shell里经常被使用到,使用阻塞模式临时启动其他程序(执行完成后才会返回):

(! "ls -ltr")

下面是另一种更简洁的方式(不过只能在命令行中使用):

!ls -ltr

感叹号 ! 必须是第一个字符. 这和 VI 编辑器的shell转义符类似. 使得不用离开newLISP 命令行就能快速调用外界程序,获得输出结果.

捕捉输出

(exec "ls /") → ("bin" "etc" "home" "lib")

修改输入

(exec "script.cgi" cgi-input)

这个例子中的 cgi-input 包含了提供给脚本的字符串数据(一般从web服务器接收). 脚本处理后的数据直接输出到屏幕上的,并不会返回给newLISP. 要通信可以使用process 或者管道.


§

17. 信号量, 共享内存

Shared memory (共享内存), semaphores (信号量) 和 processes (进程) 通常都靠互相配合来完成工作. 信号量负责进程之间的同步, 共享内负责数据传递.

下面是的演示代码比较复杂,他将三种不同的机制结合了起来.

消费者从 i = 0 到 n - 1 循环生成n个数,然后将数字依次放入共享内存,再由消费者取出. 信号量用来控制整个生产消费的过程(具体函数请看手册).

虽然用信号量和共享内存控制进程间的交互协调会非常快, 但是也非常容易产生错误(相信刚看下面信号的同学已经一个头两个大了), 特别是涉及到两个以上的进程时. 另一种更加简洁的方法就是使用 Cilk API 和 message . 18. 和 19. 章将会专门讲解.

#!/usr/bin/newlisp
# prodcons.lsp -  Producer/consumer
#
# usage of 'fork', 'wait-pid', 'semaphore' and 'share'
 
(when (= ostype "Win32")
    (println "this will not run on Win32")
    (exit))
 
(constant 'wait -1 'sig 1 'release 0)
 
(define (consumer n)
    (set 'i 0)
    (while (< i n)
        (semaphore cons-sem wait)
        (println (set 'i (share data)) " <-")
        (semaphore prod-sem sig))
    (exit))
 
(define (producer n)
    (for (i 1 n)
        (semaphore prod-sem wait)
        (println "-> " (share data i))
        semaphore cons-sem sig))
    (exit))
 
(define (run n)
    (set 'data (share))
    (share data 0)
    (set 'prod-sem (semaphore)) ; get semaphores
    (set 'cons-sem (semaphore))
    (set 'prod-pid (fork (producer n))) ; start processes
    (set 'cons-pid (fork (consumer n)))
    (semaphore prod-sem sig) ; get producer started
    (wait-pid prod-pid) ; wait for processes to finish
    (wait-pid cons-pid) ;
    (semaphore cons-sem release) ; release semaphores
    (semaphore prod-sem release))
 
(run 10)
 
(exit)

§

18. 多进程 和 Cilk

如果你的电脑是多处理器,操作系统在执行多进程任务的时候,会将各个子进程平均分布在不同的处理器上,以此来优化程序的执行. 在 newLISP 中有几个简单的API专门负责进程启动,数据同步: spawn, sync and abort ,他们都是 Cilk API 的newLISP实现(这些函数到v10.3.4为止只在*nix平台下有用,windows平台只有模拟的,具体代码可以看我的blog或者官方论坛).

从 v.10.1 版本开始可以使用 message 函数就行父子进程的通信. 更多细节请看下一章: 19. Message exchange.

启动并发进程

; calculate primes in a range
(define (primes from to)
    (local (plist)
    (for (i from to)
        (if (= 1 (length (factor i)))
        (push i plist -1)))
plist))
 
; start child processes
(set 'start (time-of-day))
 
(spawn 'p1 (primes 1 1000000))
(spawn 'p2 (primes 1000001 2000000))
(spawn 'p3 (primes 2000001 3000000))
(spawn 'p4 (primes 3000001 4000000))
 
; wait for a maximum of 60 seconds for all tasks to finish
(sync 60000) ; returns true if all finished in time
; p1, p2, p3 and p4 now each contain a lists of primes

例子中将1到4000000分成4个部分,传递给primes (搜索素数的)子进程,通过 spawn (会立即返回)并行启动,处理完的数据放到4个symbol里. 最后调用 sync 阻塞等待子进程60秒,直到子进程全部返回并存储到4个symbol里.

观察进度

当等待的时间太短而子进程又没有完成, sync 就会返回 nil. 下面循环可以用来观看进程的执行进度:

; print a dot after each 2 seconds of waiting
(until (sync 2000) (println "."))

不给 sync 提供任何参数, 他就会返回一个还在工作的进程的id列表:

; show a list of pending process ids after
;each three-tenths of a second
(until (sync 300) (println (sync)))

递归调用spawn

(define (fibo n)
    (local (f1 f2)
        (if (< n 2) 1
            (begin
                (spawn 'f1 (fibo (- n 1)))
                (spawn 'f2 (fibo (- n 2)))
                (sync 10000)
                (+ f1 f2)))))
 
(fibo 7)  → 21

事件通知

sync 设置第三个参数(一个 inlet 通知函数),我们就可以在 spawn 完成的时候得到提示.

(define (report pid)
    (println "process: " pid " has returned"))
 
; call the report function, when a child returns
(sync 10000 report)

§

19. 信息交换

父进程和使用 spawn 启动的子进程之间可以交换信息. 交换是双向的. 这就意味着父进程可以作为各个子进程之间交流的代理(通过执行子进程发来的代码,当然也可以用别的方法).

newLISP 使用 UNIX local domain sockets 构造父子进程之间的消息队列. 当接收方的队列是空的时候调用 receive 就会返回 nil. 同样当发送方的队列是满的时候, 调用send 也会返回 nil. 循环函数 until 可以让 sendreceive 就行阻塞通信.

阻塞式的发送接收信息

     ; blocking sender
     (until (send pid msg))     ; true when a msg queued up
 
     ; blocking receiver
     (until (receive pid msg))  ; true after a msg is read

阻塞式的信息交换

父进程使用 (until (receive cpid msg)) 阻塞式的 receive 每个子进程信息. (sync) 返回一个列表,其中包含了所有 spawn 出的子进程id.

#!/usr/bin/newlisp 

; child process transmits random numbers
(define (child-process)
    (set 'ppid (sys-info -4)) ; get parent pid
    (while true
        (until (send ppid (rand 100))))
)

; parent starts 5  child processes, listens and displays
; the true flag enables usage of send and receive 

(dotimes (i 5) (spawn 'result (child-process) true))

(for (i 1 3)
    (dolist (cpid (sync)) ; iterate thru pending child PIDs
        (until (receive cpid msg))
        (print "pid:" cpid "->>" (format "%-2d  " msg)))
    (println)
)

(abort) ; cancel child-processes
(exit)

输出:

pid:53181->47  pid:53180->61  pid:53179->75  pid:53178->39  pid:53177->3
pid:53181->59  pid:53180->12  pid:53179->20  pid:53178->77  pid:53177->47
pid:53181->6   pid:53180->56  pid:53179->96  pid:53178->78  pid:53177->18

非阻塞式的信息交换

非阻塞的情况下,父子进程都尽可能快的收发信息,不会阻塞住. 但同时也无法确保所有信息成功交换. 这一切都得看消息队列的长度和父进程接收处理的速度. 如果子进程的发送队列满了, (send ppid (rand 100)) 就会失败并且返回 nil.

#!/usr/bin/newlisp

; child process transmits random numbers non-blocking
; not all calls succeed
(set 'start (time-of-day))
 
(define (child-process)
    (set 'ppid (sys-info -4)) ; get parent pid
    (while true
        (send ppid (rand 100)))
)
 
; parent starts 5  child processes, listens and displays
(dotimes (i 5) (spawn 'result (child-process) true))
 
(set 'N 1000)
 
(until finished
    (if (= (inc counter) N) (set 'finished true))
    (dolist (cpid (receive)) ; iterate thru ready child pids
        (receive cpid msg)
    (if msg (print "pid:" cpid "->" (format "%-2d  \r" msg))))
)
 
(abort) ; cancel child-processes
(sleep 300)
 
(exit)

信息超时

信息的收发是可以设置超时的:

(define (receive-timeout pid msec)
    (let ( (start (time-of-day)) (msg nil))
        (until (receive pid msg)
            (if (> (- (time-of-day) start) 1000) (throw-error "timeout")))
    msg)
)
; use it
    
(receive-timeout pid 1000)  ; return message or throw error after 1 second

这个例子中信息接收将阻塞 1000 毫秒. 各种方法都可以实现超时机制.

执行信息

(鉴于一切皆数据的伟大哲学~_~)各进程都可以将接收到的信息拿来执行. 由于是在接收方的进程环境中执行, 这样一来,一个子进程就可以通过父进程将信息路由到另一个子进程. 下面的代码实现了一个信息路由:

#!/usr/bin/newlisp

; sender child process of the message
(set 'A (spawn 'result 
    (begin
        (dotimes (i 3)
            (set 'ppid (sys-info -4))
            /* the following statement in msg will be evaluated in the proxy */
            (set 'msg '(until (send B (string "greetings from " A))))
            (until (send ppid msg)))
        (until (send ppid '(begin 
            (sleep 200) ; make sure all else is printed
            (println "parent exiting ...\n")
            (set 'finished true))))) true)) 

; receiver child process of the message
(set 'B (spawn 'result 
    (begin
        (set 'ppid (sys-info -4))
        (while true
            (until (receive ppid msg))
            (println msg)
            (unless (= msg (string "greetings from " A))
                (println "ERROR in proxy message: " msg)))) true))

(until finished (if (receive A msg) (eval msg))) ; proxy loop

(abort)
(exit)

代理操作

下面看看关键代码:

; content of message to be evaluated by proxy
(until (send B (string "greetings from " A)))

进程A构造了一个列表发送给父进程,列表内部就是一个表达式,这个表达式由父进程负责执行,目的是发送进程A的ID和字符串信息给进程B. 整个过程中,父进程看起来就像一个代理,专门负责子执行子进程传递来的数据.

; the set statement is evaluated in the proxy
(until (send ppid '(set 'finished true)))

表达式 (set 'finished true) 也被发送给父进程,用来控制父进程中 until 循环的结束.

sleep 的作用是确保在所有的信息被子进程B接受并处理以后再打印 "parent exiting ...".


§

20. 数据库和列表查询

在处理少量的数据(数据项少于100个)的时候可以使用关联列表. 大型的数据应该使用 chapter 11 介绍的字典和hash.

关联列表

关联列表是一种 LISP 数据结构,用来对存储的数据进行关联检索:

; creating association lists
; pushing at the end with -1 is optimized and
; as fast as pushing in front
 
(push '("John Doe" "123-5555" 1200.00) Persons -1)
(push '("Jane Doe" "456-7777" 2000.00) Persons -1)
.....
 
Persons →  (
("John Doe" "123-5555" 1200.00)
("Jane Doe" "456-7777" 2000.00) ...)
 
; access/lookup data records
(assoc "John Doe" Persons)
 
→ ("John Doe" "123-5555" 1200.00 male)
 
(assoc "Jane Doe" Persons)
 
→ ("Jane Doe" "456-7777" 2000.00 female)

newLISP 有一个 lookup 函数,和表格软件里的很像. 这个函数功能上类似 assocnth 的结合体,能够检索出关联数据中指定位置的数据:

(lookup "John Doe" Persons 0)   → "123-555"
(lookup "John Doe" Persons -1)  → male
(lookup "Jane Doe" Persons 1)   → 2000.00
(lookup "Jane Doe" Persons -2)  → 2000.00
 
; update data records
(setf (assoc "John Doe" Persons)
    '("John Doe" "123-5555" 900.00 male))
 
; replace as a function of existing/replaced data
(setf (assoc "John Doe" Persons) (update-person $it))
 
; delete data records
(replace (assoc "John Doe" Persons) Persons)

嵌套关联列表

如果关联列表的数据部分,又包含了关联列表,他就被称为嵌套关联列表:

(set 'persons '(
    ("Anne" (address (country "USA") (city "New York")))
    ("Jean" (address (country "France") (city "Paris")))
))

和 ref 有点类似, assoc 也可以通过填写多层键标,获得嵌套关联列表内部的数据:

; one key
(assoc "Anne" persons) → ("Anne" (address (country "USA") (city "New York")))
 
; two keys
(assoc '("Anne" address) persons) → (address (country "USA") (city "New York"))
 
; three keys
(assoc '("Anne" address city) persons) → (city "New York")
 
; three keys in a vector
(set 'anne-city '("Anne" address city))
(assoc anne-city persons) → (city "New York")

当所有的键都是symbol的时候: address, country and city, 这就和 FOOP (Functional Object Oriented Programming) 非常像了. 详情请查询用户手册 "18. Functional object-oriented programming".

更新嵌套关联列表

使用 assocsetf 可以更新关联列表(嵌套的或者非嵌套的):

(setf (assoc '("Anne" address city) persons) '(city "New York")) → (city "New York")

setf 返回最新设置的值.

关联列表和hashes

Hashes 和 FOOP 对象 可以组合起来构建一个键标存取的内存数据库.

下面的例子中,数据存储在一个 hash 空间('Person)里,并通过人名访问.

setflookup 用于更新嵌套的 FOOP 对象 (另外三个context全是空的,这里只是利用了他们默认函数来构造列表):

(new Tree 'Person)
(new Class 'Address)
(new Class 'City)
(new Class 'Telephone)
 
 
(Person "John Doe" (Address (City "Small Town") (Telephone 5551234)))
 
(lookup 'Telephone (Person "John Doe"))
(setf (lookup 'Telephone (Person "John Doe")) 1234567)
(setf (lookup 'City (Person "John Doe")) (lower-case $it))
 
(Person "John Doe") → (Address (City "small town") (Telephone 1234567))

§

21. 分布式计算

如今越来越多的程序以多PC(通过网络)或者多处理器的形式分布运行(更多时候是两者结合).

通过 net-eval 函数, newLISP可以非常方便的在各个网络节点或者处理器之间,并行的执行表达式,并且通过阻塞或者事件驱动的形式回收结果.

函数 read-file, write-file, append-filedelete-file 可以很方便的访问修改(通过URL)远端文件. 而 load and save 则可以用来加载,保存远端代码.

newLISP 通过使用 HTTP 协议配合自身的命令处理功能实现分布式计算. 这就意味着可以更方便的调试,测试分布式代码(可以用终端,telnet,或者浏览器). 部署的形式也将多种多样. 举个例子可以使用 netcat (nc) 连接远端执行代码,也可以用浏览器,获取远端服务器上的web页面.

设置服务器模式

newLISP 服务端说白了就是:newLISP 进程外加端口监听.可以处理newLISP命令,也可以处理HTTP命令: GET, PUT, POST and DELETE .所以他可以用各种方式交互访问. 从 9.1 开始可以处理 CGI 请求.

newLISP 服务器有两种模式. 一种是全状态服务器,一次一个客户端,每个客户端定义的数据会被保存下来,下个客户端也可以访问. 另一种是无状态服务器, 每个客户端每次连接,都会重新启动全新的newLISP 进程,所以各种数据状态都不会保存下来.

启动全状态服务器

newlisp -c -d 4711 &
 
newlisp myprog.lsp -c -d 4711 &
 
newlisp myprog.lsp -c -w /home/node25 -d 4711 &

newLISP 启动并监听 4711端口, & (ampersand) 表示后台运行 (Unix only). -c 参数使得客户端在连接到服务器后无法看到提示符,只能看到输入的命令和执行的结果. 接下来就是等待客户端连接,然后发送命令给服务器.服务器执行完再将结果传送给客户端. 任何一个可用端口都给可以指定. 在Unix上, 开启小于1024的端口需要管理员权限.

第二个例子预先加载了一段脚本中的代码. 第三个例子使用 -w 选项指定工作目录, 如果未指定则以 newLISP 的启动目录为工作目录.

每次客户端执行完毕断开连接后, newLISP 都会重启进程,重置堆栈,信号,再回到 MAIN context. 上个客户端定义的代码和变量将会保留下来.

使用inetd启动无状态服务器

在 Unix 上 inetd 或者 xindetd 能够用来启动一个无状态服务器. 这时候 TCP/IP 由Unix 负责管理,可以同时处理多个连接. 每接受一个客户端连接 inetd 或者 xinetd 就会启动一个newLISP 进程. 客户端断开,进程就会被关闭.

如果节点间不需要保存状态, 以这种方式启动 newLISP 服务区是最好的, 因为同时处理多项业务大大提高了效率.

inetd 或者 xinetd 的配置文件一般存放在 /etc 目录.

将下面的内容附加到 /etc/services 里:

net-eval        4711/tcp     # newLISP net-eval requests

注意除了 4711 ,别的端口也可以指定.

接下来配置 inetd ,将下面几行选一个附加到 /etc/inetd.conf 文件:

net-eval  stream  tcp  nowait  root  /usr/bin/newlisp -c
 
# as an alternative, a program can also be preloaded
 
net-eval  stream  tcp  nowait  root  /usr/bin/newlisp myprog.lsp -c
 
# a working directory can also be specified
 
net-eval  stream  tcp  nowait  newlisp  /usr/bin/newlisp -c -w /usr/home/newlisp

最后一行使用 newlisp 替代 root 用户,同时工作目录也改了. 这样的好处就是限制了客户端的权限(只要设置好newlisp这个用户的权限就行了),使得服务器更安全.

在一些 Unix 上可以使用一个更现代的 inetd: xinetd . 将下面的配置内容加入 /etc/xinet.d/net-eval:

service net-eval
    {
    socket_type = stream
    wait = no
    user = root
    server = /usr/bin/newlisp
    port = 4711
    server_args = -c -w /home/node
    }

多个参数配合起来可以限制客户端访问的目录和访问的权限. 信息内容请查询 inetdxinetd 手册.

在配置完 inetd or xinetd 后必须重启他们,才能使新配置文件生效. 可以发送 HUP 信号给系统, 也可以使用发送 kill 或者 nohupinetd or xinetd 进程.

在 Mac OS X 上的 launchd 可以起到类似的作用. newLISP 的源码包里有一个 org.newlisp.newlisp.plist ( util/目录)文件. 就是用来配置 OS 在启动的时候加载newLISP的.

使用telnet测试服务器

下面我们用 telnet 连接服务器看看:

telnet localhost 4711
 
; or when running on a different computer i.e. ip 192.168.1.100
 
telnet 192.168.1.100 4711

如果输入多行代码,要在开始的第一行单独写上 [cmd],同时在结尾的最后一行单独写上 [/cmd].虽然从 newLISP v10.3.0 开始可以使用新的多行标示符,但在使用 netcat 或者别的工具的时候, 多行表达式还是必须用 [cmd], [/cmd] .

使用netcat测试服务器

echo '(symbols) (exit)' | nc localhost 4711

或者连接到远程服务器:

echo '(symbols) (exit)' | nc 192.168.1.100 4711

上面两个例子 netcat 都会回显 (symbols) 的执行结果.

同样要写入多行代码时也必须用 [cmd], [/cmd].(其实可以直接nc 连接上去,就和newLISP shell里执行一样)

使用命令行测试服务器

net-eval 函数可以连接远程newLISP服务端 ,然后发送命令给服务器执行,最后获取数据到本地. 这个模式是专门为了命令行测试而设计的:

(net-eval "localhost" 4711 "(+ 3 4)" 1000) → 7
 
; to a remote node
 
(net-eval "192.168.1.100" 4711 {(upper-case "newlisp")} 1000) → "NEWLISP"

第二个例子中的花括号 {,} 用来限制代码在本地执行. 花括号内部的引号也被转义了,整个花括号的内容就是一个字符串.

如果要输入多行代码可以不使用 [cmd], [/cmd] 标示符,因为 net-eval 内部本身就支持多行代码传输.

使用浏览器测试WEB服务

newLISP 服务器也可以理解简单的 HTTP 请求 GET and PUT. 不过记得输入完整的路径:

http://localhost:4711//usr/share/newlisp/doc/newlisp_manual.html

手册大概有 800 K,浏览器加载会花些微时间. 端口和主机名之间必须用冒号分开. 如果是在NIX服务器上,端口号后面那两个斜杠是必须的(/是根目录嘛).

分布式的简单样例

在 newLISP 服务端测试成功后, 我们就可以发送数据给远端执行了. 通常一个大的代码段会被划分成几个小的代码段,分送给不同的服务端执行.

第一个例子没什么实质性的价值,只是执行几个简单的代码,用来论证分布式的工作原理:

#!/usr/bin/newlisp
 
(set 'result (net-eval '(
    ("192.168.1.100" 4711 {(+ 3 4)})
    ("192.168.1.101" 4711 {(+ 5 6)})
    ("192.168.1.102" 4711 {(+ 7 8)})
    ("192.168.1.103" 4711 {(+ 9 10)})
    ("192.168.1.104" 4711 {(+ 11 12)})
) 1000))
 
 
(println "result: " result)
 
(exit)

程序的输出结果如下:

result: (7 11 15 19 23)

如果你使用的是UNIX,那你就可以使用 inetd or xinetd 来配置 newLISP 服务器了,他们会产生5个 无状态的newLISP进程.同时可以将所有的IP地址替换成 "localhost" 或者另一个相同的IP地址,这样所有的任务就可以在一个CPU完成.在 Win32 上可以启动5个全状态的服务器达到相同的目的(我的扫描器就是用的这种方法,当初有人建议作者为win32 开发多进程的时候,作者提出了一个socket 异步替代的方法,那时我还没看到这里,所以也没用到net-eval,而是直接用的代码预加载.其实可用这种方法模拟win32的多进程,因为是全状态,加上souce,load).

可以给 net-eval 提供一个回调函数, 在处理数据可读的时候:

#!/usr/bin/newlisp
 
(define (idle-loop p)
    (if p (println p)))
 
(set 'result (net-eval '(
    ("192.168.1.100" 4711 {(+ 3 4)})
    ("192.168.1.101" 4711 {(+ 5 6)})
    ("192.168.1.102" 4711 {(+ 7 8)})
    ("192.168.1.103" 4711 {(+ 9 10)})
    ("192.168.1.104" 4711 {(+ 11 12)})
) 1000 idle-loop))
 
(exit)

net-eval 将每一个服务端执行的结果,作为参数 p 传递给 idle-loop 处理. 如果执行超时(例子中是1000毫秒),参数 p 就是 nil ,否则将是一个列表,其中包含了服务端的地址,端口,和执行结果. 最后输出的内容如下:

("192.168.1.100" 4711 7)
("192.168.1.101" 4711 11)
("192.168.1.102" 4711 15)
("192.168.1.103" 4711 19)
("192.168.1.104" 4711 23)

如果要测试一个 CPU, 请将所有地址替换成 "localhost"; Unix 上 inetd or xinetd daemon 会自动产生5个独立的进程,并同时监听端口 4711. Win32上则需要自己启动5个全状态服务器,并指定5个不同的端口.

设置 'net-eval' 参数的结构

在网络状态下,远程服务端的ip会经常的变动, 这时候将节点参数放进一个专门的列表就显得非常必要了. 下面的例子将节点信息和,代码信息,单独分开来写. 并且成功的将不同的代码分配给不同的服务端执行:

#!/usr/bin/newlisp
 
; node parameters
(set 'nodes '(
    ("192.168.1.100" 4711)
    ("192.168.1.101" 4711)
    ("192.168.1.102" 4711)
    ("192.168.1.103" 4711)
    ("192.168.1.104" 4711)
))
 
; program template for nodes
(set 'program [text]
    (begin
        (map set '(from to node) '(%d %d %d))
        (for (x from to)
		(if (= 1 (length (factor x)))
        (push x primes -1)))
    primes)
[/text])
 
; call back routine for net-eval
(define (idle-loop p)
    (when p
        (println (p 0) ":" (p 1))
        (push (p 2) primes))
)
 
(println "Sending request to nodes, and waiting ...")
 
; machines could be on different IP addresses.
; For this test 5 nodes are started on localhost
(set 'result (net-eval (list
    (list (nodes 0 0) (nodes 0 1) (format program 0 99999 1))
    (list (nodes 1 0) (nodes 1 1) (format program 100000 199999 2))
    (list (nodes 2 0) (nodes 2 1) (format program 200000 299999 3))
    (list (nodes 3 0) (nodes 3 1) (format program 300000 399999 4))
    (list (nodes 4 0) (nodes 4 1) (format program 400000 499999 5))
) 20000 idle-loop))
 
(set 'primes (sort (flat primes)))
(save "primes" 'primes)
 
(exit)

开始的 nodes 列表包含了所有节点的地址和端口信息.

program 负责提取指定范围内的素数. 代码中的 from, tonode 三个变量是通过 format 传递进去的. 因为使用了 begin , 所以只有最后一个表达式(primes)的值会从服务端返回.

有很多方法能够配置 net-eval 的参数:

(set 'node-eval-list (list
    (list (nodes 0 0) (nodes 0 1) (format program 0 99999 1))
    (list (nodes 1 0) (nodes 1 1) (format program 100000 199999 2))
    (list (nodes 2 0) (nodes 2 1) (format program 200000 299999 3))
    (list (nodes 3 0) (nodes 3 1) (format program 300000 399999 4))
    (list (nodes 4 0) (nodes 4 1) (format program 400000 499999 5))
))
 
(set 'result (net-eval node-eval-list  20000 idle-loop))

函数 idle-loop 负责收集处理所有素数:

192.168.1.100:4711
192.168.1.101:4711
192.168.1.102:4711
192.168.1.103:4711
192.168.1.104:4711

在程序真正的部署应用之前,可以先在一个单独的CPU上测试下: 把所有的ip地址全部替换成 "localhost" 或者别的单独的主机名和IP地址.

传输文件

在newLISP 可以使用读写本地文件的函数(read write),读写远程文件(目前只限于UNIX). 之所以能这样使用,是因为这些函数内部实现了HTTP协议中的 GETPUT 操作. 注意只有少量的Apache服务器默认允许 PUT .

下面的这三个函数可以通过URL连接读写远程newLISP服务器上的文件: read-file, write-fileappend-file :

(write-file "http://127.0.0.1:4711//Users/newlisp/afile.txt" "The message - ")
→ "14 bytes transferred for /Users/newlisp/afile.txt\r\n"
 
(append-file "http://127.0.0.1:4711//Users/newlisp/afile.txt" "more text")
→ "9 bytes transferred for /Users/newlisp/afile.txt\r\n"
 
(read-file "http://127.0.0.1:4711//Users/newlisp/afile.txt")
→ "The message - more text"

前两个函数返回修改的字节数和文件名. 第三个函数 read-file 返回读取的内容.

如果发生错误,将会返回一条以 ERR: 开头的错误信息:

(read-file "http://127.0.0.1:4711//Users/newlisp/somefile.txt")
→ "ERR:404 File not found: /Users/newlisp/somefile.txt\r\n"

如果使用的是UNIX系统,记得在端口号后要写两个反斜杠(绝对路径).

所有函数都可以传输二进制内容(包含0). 在 newLISP 内部这三个函数会被 get-urlput-url 替代. 这两个函数的参数和上面的三个通用. 更多细节请爬手册.

加载和保存数据

前面介绍的 loadsave 的函数也可以用来加载和保存远程端点(newLISP 服务器)的内容.

记得用 URL 标示好远程文件:

(load "http://192.168.1.2:4711//usr/share/newlisp/mysql5.lsp")
 
(save "http://192.168.1.2:4711//home/newlisp/data.lsp" 'db-data)

尽管 loadsave 函数内部使用 get-urlput-url 来完成远程文件的读写操作, 但是在我们使用的时候就和操作本地文件没什么差别,除了使用的文件名是URL形式的. 两个函数的超时间都是60秒. 如果需要更精确的控制,可以用 get-url and put-url 结合 eval-string and source 模拟实现 load and save 的 HTTP 模式.

Unix本地域套接字

newLISP 服务端支持 本地域套接字,配合网络函数 net-eval, net-listen, net-connectnet-accept, net-receive, net-select and net-send 一起使用,

可以加快了服务器与同一文件系统内其他进程之间的通信速度. 具体细节请查手册.


§

22. HTTPD web服务器唯一模式

前几章我们都用 -c 启动服务器, 这种服务器可以同时应答 net-evalHTTP 请求,并且可以传输文本和二进制文件. 在可信任网络里(比如安全防火墙隔离后的网络)用这种模式,既方便快捷又功能强大,是不二的选择. 如果是在Internet里我们则可以使用另一种相对安全的模式 -http 模式, 这种模式只处理 HTTP 请求: GET, PUT, POST and DELETECGI 请求. 在真正部署网站之前,记得做好目录控制,文件限制.等安全加固工作.

环境变量

无论是 -c 还是 -http 下面的变量默认都会被设置 DOCUMENT_ROOT, REQUEST_METHOD, SERVER_SOFTWARE and QUERY_STRING . 下面的变量只有在客户端请求的HTTP 头中出现后才会设置 CONTENT_TYPE, CONTENT_LENGTH, HTTP_HOST, HTTP_USER_AGENT and HTTP_COOKIE .

预处理请求

newLISP server 在面对各种请求的时候 (HTTP and command line), 可以使用 command-event 对请求信息进行预处理. 预处理函数可以通过在启动服务器的时候加载 httpd-conf.lsp 获得:

server_args = httpd-conf.lsp -http -w /home/node

上面的代码是 xinetd 配置文件的一部分. 一旦newLISP 被调用, 就会加载 httpd-conf.lsp 文件. -c 选项被 -http 替换. 现在除了HTTP 请求以外,任何的 net-eval 或者 命令行请求都会被服务器忽略.

启动参数也可以以命令行的形式直接传递给newlisp, 下面的启动文件 httpd-conf.lsp 和 newlisp 同一目录:

newlisp httpd-conf.lsp -http -d 80 -w /home/www &

所有的请求信息都会被 command-event 设置的过滤函数过滤:

; httpd-conf.lsp
;
; filter and translate HTTP request for newLISP
; -c or -http server modes
; reject query commands using CGI with .exe files
 
(command-event (fn (s)
    (local (request)
    (if (find "?" s) ; is this a query
        (begin
            (set 'request (first (parse s "?")))
            ; discover illegal extension in queries
            (if (ends-with request ".exe")
                (set 'request "GET /errorpage.html")
                (set 'request s)))
        (set 'request s))
    request)
))
 ; eof

如果请求的CGI文件是以 .exe 结尾的,将会被拒绝,并返回一个错误页面.

HTTP模式下的CGI处理

在 http://www.newlisp.org 上可以找到很多的 CGI 的使用样例. http://www.newlisp.org/downloads 提供了两个相对比较复杂的样例: newlisp-ide (一个基于web开发的ide) 和 newlisp-wiki (一个内容管理系统,目前已经在 [http://www.newlisp.org www.newlisp.org] 上运行).

CGI 程序必须以 .cgi 结尾,同时拥有执行期限(ON UNIX).

下面是一个迷你版的CGI程序:

#!/usr/bin/newlisp
 
(print "Content-type: text/html\r\n\r\n")
(println "<h2>Hello World</h2>")
(exit)

newLISP 服务器一般会给客户端发送一个标准的HTTP头, HTTP/1.0 200 OK\r\n ,外加一行服务器申明 Server: newLISP v. ...\r\n . 如果 CGI程序的首行是 "Status:" , 那么newLISP 就不会返回标准的HTTP 头了, 而是由CGI程序自己提供一个完整的HTTP头. 下面的例子返回一个重定向的网址:

#!/usr/bin/newlisp
(print "Status: 301 Moved Permanently\r\n")
(print "Location: http://www.newlisp.org/index.cgi\r\n\r\n")
(exit)

当你的newLISP 安装完以后,在modules目录下就可以找到 cgi.lsp. 这个模块包含了很多实用的CGI操作代码,比如分解HTTP(GET and POST)请求参数, 分解设置 cookies 信息. 更多内容可以查阅官方网站: http://www.newlisp.org/modules/.

HTTP模式中的文件类型

无论是 -c 还是 -http 下面的文件类型都能够可以被服务器识别, 也能够被服务器发送给客户端(通过设置HTTP 头的 Content-Type: ):

file extension media type
.avi video/x-msvideo
.css text/css
.gif image/gif
.htm text/htm
.html text/html
.jpg image/jpg
.js application/javascript
.mov video/quicktime
.mp3 audio/mpeg
.mpg video/mpeg
.pdf application/pdf
.png image/png
.wav audio/x-wav
.zip application/zip
any other text/plain

§

23. newLISP 扩展

newLISP 的 import 函数, 可以用来导入各种库函数: DLLs (Win32的动态链接库), Linux/Unix 的共享库 (在 Mac OS X 中以 .so 或者 .dylib结尾).

简单的 FFI 扩展接口

从 10.3.8/9/10 开始 newLISP 加入了一个扩展语法(具体看后面的代码), 供下面的扩展函数使用: import, callback , struct , packunpack . 扩展语法只支持使用 libffi 库构建的 newLISP 版本. 在 www.newlisp.org 上发布的二进制版本都支持这个语法.

扩展语法, 让我们可以在newLISP中使用C语言中的数据类型, 为导入函数指定参数和返回值(有点类似C中的函数申明,指定数据类型就行了). 好处就是编码的时候只要将newLISP数据直接传递给调用函数就可以了,剩下转换工作留给ffi. 另外还可以将newLISP函数,注册为callback(回调函数),提供给导入函数调用. 扩展语法虽然也支持浮点数和结构,不过不建议使用浮点数. Handling of floating point types was either impossible or unreliable using the simple API that depended on pure cdecl calling conventions. These are not available on all platforms. (简而言之,浮点数不可靠,而且不是所有系统都支持). 扩展API会根据不同的CPU架构,自动为转换后类型的数据做对齐操作. 具体的扩展语法请看用户手册 User Manual and Reference.

下面介绍的API大部分还可以在新版本中使用. 具体的函数介绍请查手册 User Manual and Reference : import, callback, struct, pack and unpack.

用C写共享库

接下来演示如何在Win32 and Linux/Unix 上用C语言编写和编译共享库.

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
 
int foo1(char * ptr, int number)
     {
     printf("the string: %s the number: %d\n", ptr, number);
     return(10 * number);
     }
 
char * foo2(char * ptr, int number)
     {
     char * upper;
     printf("the string: %s the number: %d\n", ptr, number);
     upper = ptr;
     while(*ptr) { *ptr = toupper(*ptr); ptr++; }
     return(upper);
     }
 
/* eof */

foo1 和 foo2 两个函数都会打印他们的参数, foo1将参数中的数字乘以 10 返回, foo2 将参数中的字符串做大写转换之后返回(之后将会在newLISP中读取这个字符串).

在UNIX上编译共享库

On Mac OS X and Linux/Unix 使用下面的方法编译 testlib.so :

gcc testlib.c -shared -o testlib.so

Or On Mac OSX/darwin 用下面的方法:

gcc testlib.c -bundle -o testlib.dylib

testlib.so 构建的时候做了 cdecl 转换(NIX默认). 用下面的方法导入库函数(NIX和WIN32差不多,只要指定好库路径就是了):

> (import "/home/newlisp/testlib.so" "foo1")
foo1 <6710118F>
 
> (import "/home/newlisp/testlib.so" "foo2")
foo2 <671011B9>
 
> (foo1 "hello" 123)
the string: hello the number: 123
1230
 
> (foo2 "hello" 123)
the string: hello the number: 123
4054088
 
> (get-string (foo2 "hello" 123))
the string: hello the number: 123
"HELLO"
>

foo2 返回了一个字符串的指针, 用get-string就可以读取指针指向的内容(遇到空字符就返回). 至于别的非字符串的数据类型(包含了0的)可以用unpack.

在Win32上编译DLL

DLL 可以用 MinGW, Borland or CYGWIN 编译. 下面的例子使用的是 MinGW (很多ide都有默认的dll模板).

编译:

gcc -c testlib.c -o testlib.o

在将 testlib.o 转换成 DLL 之前需要写一个 testlib.def 用来申明导出函数:

LIBRARY  testlib.dll
EXPORTS
         foo1
         foo2

现在转换DLL:

dllwrap testlib.o --def testlib.def -o testlib.dll -lws2_32

testlib.dll构造的时候做了 stdcall 转换(Win32 默认). 下面演示如果调用这个库:

> (import "testlib.dll" "foo1")
foo1 <6710118F>
 
> (import "testlib.dll" "foo2")
foo2 <671011B9>
 
> (foo1 "hello" 123)
the string: hello the number: 123
1230
 
> (foo2 "hello" 123)
the string: hello the number: 123
4054088
 
> (get-string (foo2 "hello" 123))
the string: hello the number: 123
"HELLO"
 
>
; import a library compiled for cdecl
; calling conventions
> (import "foo.dll" "func" "cdecl")

注意: foo2 返回的 4054088 字符串的内存地址. 要用get-string才能读取字符串内容. 因为newLISP会根据系统自动判断库函数的调用类型, 在WIN32下,默认的函数调用类型是 stdcall,所以如果你在 WIN32 上调用的库是 cdecl 的, 就必须用 cdecl 关键字申明.

导入数据结构

'C' 结构和 'C' 字符串一样都可以通过指针操作访问,'C' 结构中的成员可以使用 get-string, get-int or get-char 访问. 下面来看样例:

typedef struct mystruc
   {
   int number;
   char * ptr;
   } MYSTRUC;
 
MYSTRUC * foo3(char * ptr, int num )
   {
   MYSTRUC * astruc;
   astruc = malloc(sizeof(MYSTRUC));
   astruc->ptr = malloc(strlen(ptr) + 1);
   strcpy(astruc->ptr, ptr);
   astruc->number = num;
   return(astruc);
   }

可以使用下面的方法访问结构成员(不是唯一的方法):

> (set 'astruc (foo3 "hello world" 123))
4054280
 
> (get-string (get-integer (+ astruc 4)))
"hello world"
 
> (get-integer astruc)
123
>

foo3 返回的是结构 astruc 的地址. (+ astruc 4)是字符串的地址, 在32位OS里 'C' 语言的一个整数4个字节的内存(MYSTRUC 的 int number). 字符串通过 get-string 访问.

内存管理

如上所示, 如果某些导入函数申请了内存却没有释放. 就必须用 libc 里的函数 free 来释放这些申请的数据:

(import "/usr/lib/libc.so" "free")

(free astruc) ; astruc contains memory address of allocated structure

如果传递给导入函数的数据,是newLISP数据的引用, 就不需要手工释放内存了(因为newLISP会做).

结构对齐

为了加快数据处理速度(便于寄存器读取). 很多数据会被自动对齐:

struct mystruct
    {
    short int x;
    int z;
    short int y;
    } data;
 
struct mystruct * foo(void)
    {
    data.x = 123;
    data.y = 456;
    data.z = sizeof(data);
    return(&data);
    }

变量 xy 各占用 16 位(bit) z 占用 32 位(bit). 在32-位 CPU 的系统上,编译器会自动将x和y扩充成32位的. 这样就能和32位的z对齐. 鉴于这种情况,就必须用下面的方法访问变量x和y了:

> (import "/usr/home/nuevatec/test.so" "foo")
foo <281A1588>
 
> (unpack "lu lu lu" (foo))
(123 12 456)

整个结构消耗了 3 by 4 = 12 字节, 因为所有的成员都自动对齐到了32位(4个字节)的宽度.

下面的结构共占用 8 个字节(byte): 变量 xy 各2个, 变量 z 占用 4 个字节(byte). 因为 x and y 合起来一共32位可以一次性读取(寄存器是32位的)所以没必要再对齐了:

struct mystruct
     {
     short int x;
     short int y;
     int z;
     } data;
 
 struct mystruct * foo(void)
    {
    data.x = 123;
    data.y = 456;
    data.z = sizeof(data);
    return(&data);
    }

这时候就可以用成员本身的大小去访问了(还有种通用的方法不用改变unpack参数 (struct 'ok "short int" "int" "short int") (unpack ok (foo))):

> (import "/usr/home/nuevatec/test.so" "foo")
foo <281A1588>
 
> (unpack "u u lu" (foo))
(123 456 8)

参数传递

data Type newLISP call C function call
integer (foo 123) foo(int number)
double float (foo 1.234) foo(double number)
float (foo (flt 1.234)) foo(float number)
string (foo "Hello World!") foo(char * string)
integer array (foo (pack "d d d" 123 456 789)) foo(int numbers[])
float array (foo (pack "f f f" 1.23 4.56 7.89)) foo(float[])
double array (foo (pack "lf lf lf) 1.23 4.56 7.89) foo(double[])
string array (foo (pack "lu lu lu" "one" "two" "three"))) foo(char * string[])

注意 floatsdouble floats 只适用于 x86系统的 cdecl 调用或者作为引用传递给函数, i.e: printf(). 更多浮点数(单,双精度)和'C' 结构成员的操作方法, 请看下面函数的介绍: import, callback and struct .

pack 可以从一个列表中获取多个参数用来格式化:

 
(pack "lu lu lu" '("one" "two" "three"))

提取返回值

data Type newLISP to extract return value C return
integer (set 'number (foo x y z)) return(int number)
double float n/a - only 32bit returns, use double float pointer instead not available
double float ptr (set 'number (get-float (foo x y z))) return(double * numPtr)
float not available not available
string (set 'string (get-string (foo x y z) return(char * string)
integer array (set 'numList (unpack "ld ld ld" (foo x y z))) return(int numList[])
float array (set 'numList (unpack "f f f" (foo x y z))) return(float numList[])
double array (set 'numList (unpack "lf lf lf") (foo x y z))) return(double numList[])
string array (set 'stringList (map get-string (unpack "ld ld ld" (foo x y z)))) return(char * string[])

Floatsdoubles 只能通过地址操作(任何get- 函数操作的都是地址).

如果返回的是数组,则必须知道数组长度才能提取. 例子假设的长度都是三.

所有的 pack 和 unpack 格式化字符之间也可以没有空格,不过那样可读性会很差(比如(unpack "fldb" (foo x y z)) 是不是很蛋疼).

"ld""lu" 可以互换(这里有疑惑,地址是可以但是数据...), 但是16位的 "u""d" 可能会产生不同的结果, 因为从 16 到 32 位是正数增长.

packunpack 可以在大端和小端之间切换.

编写封装库

import 有时候无法导入库. 这是因为库没有遵循正规的 cdecl 调用,将所有的参数压入堆栈. 比如将 Mac OS X 运行在老式的 PPC CPUs 上, 默认安装的OpenGL库将无法使用.

这时可以写一个专门的封装库将 cdecl 调用转换成目标库的调用方式.

/* wrapper.c - demo for wrapping library function
 
compile:
    gcc -m32 -shared wrapper.c -o wrapper.so
or:
    gcc -m32 -bundle wrapper.c -o wrapper.dylib
 
usage from newLISP:
 
    (import "./wrapper.dylib" "wrapperFoo")
 
    (define (glFoo x y z)
        (get-float (wrapperFoo 5 (float x) (int y) (float z))) )
 
 (glFoo 1.2 3 1.4) => 7.8
 
*/
 
#include <stdio.h>
#include <stdarg.h>
 
 
/* the glFoo() function would normally live in the library to be
   wrapped, e.g. libopengl.so or libopengl.dylib, and this
   program would link to it.
   For demo and test purpose the function has been included in this
   file
*/
 
double glFoo(double x, int y, double z)
    {
    double result;
 
    result = (x + z) * y;
 
    return(result);
    }
 
/* this is the wrapper for glFoo which gets imported by newLISP
   declaring it as a va-arg function guarantees 'cdecl'
   calling conventions on most architectures
*/
 
double * wrapperFoo(int argc, ...)
    {
    va_list ap;
    double x, z;
    static double result;
    int y;
 
    va_start(ap, argc);
 
    x = va_arg(ap, double);
    y = va_arg(ap, int);
    z = va_arg(ap, double);
 
    va_end(ap);
 
    result = glFoo(x, y, z);
    return(&result);
    }
 
/* eof */

为扩展库注册回调函数

newLISP的内建函数 callback 可以获取用户定义函数的地址, 并将他注册成库函数的回调函数:

(define (keyboard key x y)
    (if (= (& key 0xFF) 27) (exit)) ; exit program with ESC
    (println "key:" (& key 0xFF) " x:" x  " y:" y))

(glutKeyboardFunc (callback 1 'keyboard))

上面的代码来之 opengl-demo.lsp ( 源码版 newlisp-x.x.x/examples/ ). 还有个 win32demo.lsp ,演示了windows事件循环.

更多 callback 高级用法请看 newLISP User Manual and Reference.


§

24. newLISP共享库

在所有的系统上, newLISP 都可以被编译成共享库. 在 Win32 上是 newlisp.dll, 在 Mac OS X 上是 newlisp.dylib ,在 Linux 和 BSDs 上是 newlisp.so. Makefiles 文件可以在源码包中找到. 只有在 Win32 上的安装程序才会附带预先编译好的 newlisp.dll , 并且将之安装到系统目录 WINDOWS\system32\ (从 v.10.3.3 安装到 Program Files/newlisp/ 目录).

利用共享库执行代码

第一个例子我们用newLISP 调用自己的共享库函数 newlispEvalStr:

(import "/usr/lib/newlisp.so" "newlispEvalStr")
(get-string (newlispEvalStr "(+ 3 4)")) →  "7\n"

newlispEvalStr 的返回值总是一个字符串指针. 必须用 get-string 获得其中的内容. 所有的结果,就算是数字,都会以字符串(加换行符)的形式返回.如果不需要输出可以用 silent 函数屏蔽. 要得到数字,可以通过 int 或者 float 转换.

要传递多行代码必须用 [cmd], [/cmd]:

(set 'code [text][cmd]
...
...
...
[/cmd][/text])

下面演示如何在C程序中调用 newlispEvalStr :

/* libdemo.c - demo for importing newlisp.so
 * 
 * compile using: 
 *    gcc -ldl libdemo.c -o libdemo
 *
 * use:
 *
 *    ./libdemo '(+ 3 4)'
 *    ./libdemo '(symbols)'
 *
 */
#include <stdio.h>
#include <dlfcn.h>
 
int main(int argc, char * argv[])
{
void * hLibrary;
char * result;
char * (*func)(char *);
char * error;
 
if((hLibrary = dlopen("/usr/lib/newlisp.so",
                       RTLD_GLOBAL | RTLD_LAZY)) == 0)
    {
    printf("cannot import library\n");
    exit(-1);
    }
 
func = dlsym(hLibrary, "newlispEvalStr");
if((error = dlerror()) != NULL)
    {
    printf("error: %s\n", error);
    exit(-1);
    }
 
printf("%s\n", (*func)(argv[1]));
 
return(0);
}

/* eof */

程序接受newLISP表达式, 并打印执行后的结果.

注册回调函数

newLISP 库里的 newlispCallback 函数可以用来注册回调函数. 下面的代码中newLISP 调用自己共享库中的注册函数注册了一个回调函数 callme:

#!/usr/bin/newlisp

; path-name of the library depending on platform
(set 'LIBRARY (if (= ostype "Win32") "newlisp.dll" "newlisp.dylib"))

; import functions from the newLISP shared library
(import LIBRARY "newlispEvalStr")
(import LIBRARY "newlispCallback")

; set calltype platform specific
(set 'CALLTYPE (if (= ostype "Win32") "stdcall" "cdecl"))

; the callback function
(define (callme p1 p2 p3 result)
    (println "p1 => " p1 " p2 => " p2 " p3 => " p3)
    result)

; register the callback with newLISP library
(newlispCallback "callme" (callback 0 'callme) CALLTYPE)

; the callback returns a string
(println (get-string (newlispEvalStr
    {(get-string (callme 123 456 789 "hello world"))})))

; the callback returns a number
(println (get-string (newlispEvalStr
    {(callme 123 456 789 99999)})))

返回值不同, 处理的代码也必须不同:

p1 => 123 p2 => 456 p3 => 789
"hello world"

p1 => 123 p2 => 456 p3 => 789
99999

注意: Win32 和多数 Unix 可以把库文件放在系统目录, Mac OS X 则要将 newlisp.dylib 放在调用程序的当前目录,当然你也可以使用绝对路径.


§



GNU Free Documentation License

Version 1.2, November 2002

Copyright (C) 2000,2001,2002 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.



0. PREAMBLE

The purpose of this License is to make a manual, textbook, or other functional and useful document "free" in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others.

This License is a kind of "copyleft", which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software.

We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference.

1. APPLICABILITY AND DEFINITIONS

This License applies to any manual or other work, in any medium, that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. Such a notice grants a world-wide, royalty-free license, unlimited in duration, to use that work under the conditions stated herein. The "Document", below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as "you". You accept the license if you copy, modify or distribute the work in a way requiring permission under copyright law.

A "Modified Version" of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modifications and/or translated into another language.

A "Secondary Section" is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Document's overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (Thus, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical or political position regarding them.

The "Invariant Sections" are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License. If a section does not fit the above definition of Secondary then it is not allowed to be designated as Invariant. The Document may contain zero Invariant Sections. If the Document does not identify any Invariant Sections then there are none.

The "Cover Texts" are certain short passages of text that are listed, as Front-Cover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License. A Front-Cover Text may be at most 5 words, and a Back-Cover Text may be at most 25 words.

A "Transparent" copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, that is suitable for revising the document straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup, or absence of markup, has been arranged to thwart or discourage subsequent modification by readers is not Transparent. An image format is not Transparent if used for any substantial amount of text. A copy that is not "Transparent" is called "Opaque".

Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo input format, LaTeX input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML, PostScript or PDF designed for human modification. Examples of transparent image formats include PNG, XCF and JPG. Opaque formats include proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML, PostScript or PDF produced by some word processors for output purposes only.

The "Title Page" means, for a printed book, the title page itself, plus such following pages as are needed to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, "Title Page" means the text near the most prominent appearance of the work's title, preceding the beginning of the body of the text.

A section "Entitled XYZ" means a named subunit of the Document whose title either is precisely XYZ or contains XYZ in parentheses following text that translates XYZ in another language. (Here XYZ stands for a specific section name mentioned below, such as "Acknowledgements", "Dedications", "Endorsements", or "History".) To "Preserve the Title" of such a section when you modify the Document means that it remains a section "Entitled XYZ" according to this definition.

The Document may include Warranty Disclaimers next to the notice which states that this License applies to the Document. These Warranty Disclaimers are considered to be included by reference in this License, but only as regards disclaiming warranties: any other implication that these Warranty Disclaimers may have is void and has no effect on the meaning of this License.

2. VERBATIM COPYING

You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in section 3.

You may also lend copies, under the same conditions stated above, and you may publicly display copies.

3. COPYING IN QUANTITY

If you publish printed copies (or copies in media that commonly have printed covers) of the Document, numbering more than 100, and the Document's license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects.

If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages.

If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a computer-network location from which the general network-using public has access to download using public-standard network protocols a complete Transparent copy of the Document, free of added material. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public.

It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document.

4. MODIFICATIONS

You may copy and distribute a Modified Version of the Document under the conditions of sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version:

If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version's license notice. These titles must be distinct from any other section titles.

You may add a section Entitled "Endorsements", provided it contains nothing but endorsements of your Modified Version by various parties--for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard.

You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front-Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one.

The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsement of any Modified Version.

5. COMBINING DOCUMENTS

You may combine the Document with other documents released under this License, under the terms defined in section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice, and that you preserve all their Warranty Disclaimers.

The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work.

In the combination, you must combine any sections Entitled "History" in the various original documents, forming one section Entitled "History"; likewise combine any sections Entitled "Acknowledgements", and any sections Entitled "Dedications". You must delete all sections Entitled "Endorsements."

6. COLLECTIONS OF DOCUMENTS

You may make a collection consisting of the Document and other documents released under this License, and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects.

You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document.

7. AGGREGATION WITH INDEPENDENT WORKS

A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, is called an "aggregate" if the copyright resulting from the compilation is not used to limit the legal rights of the compilation's users beyond what the individual works permit. When the Document is included in an aggregate, this License does not apply to the other works in the aggregate which are not themselves derivative works of the Document.

If the Cover Text requirement of section 3 is applicable to these copies of the Document, then if the Document is less than one half of the entire aggregate, the Document's Cover Texts may be placed on covers that bracket the Document within the aggregate, or the electronic equivalent of covers if the Document is in electronic form. Otherwise they must appear on printed covers that bracket the whole aggregate.

8. TRANSLATION

Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License, and all the license notices in the Document, and any Warranty Disclaimers, provided that you also include the original English version of this License and the original versions of those notices and disclaimers. In case of a disagreement between the translation and the original version of this License or a notice or disclaimer, the original version will prevail.

If a section in the Document is Entitled "Acknowledgements", "Dedications", or "History", the requirement (section 4) to Preserve its Title (section 1) will typically require changing the actual title.

9. TERMINATION

You may not copy, modify, sublicense, or distribute the Document except as expressly provided for under this License. Any other attempt to copy, modify, sublicense or distribute the Document is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.

10. FUTURE REVISIONS OF THIS LICENSE

The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http://www.gnu.org/copyleft/.

Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License "or any later version" applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation.