Author: ctian Date: Wed Sep 5 12:50:59 2007 New Revision: 31
Added: books/onlisp/Makefile Modified: books/onlisp/3-functional_programming.tex books/onlisp/onlisp.tex Log: chap 3, in progress
Modified: books/onlisp/3-functional_programming.tex ============================================================================== --- books/onlisp/3-functional_programming.tex (original) +++ books/onlisp/3-functional_programming.tex Wed Sep 5 12:50:59 2007 @@ -1,17 +1,429 @@ -\chapter{函数型编程} +\chapter{函数式编程} \label{chap:functional_programming}
-前一章解释了 Lisp 和 Lisp 程序是怎样构建自单一的基本材料: 函数. 与任何建筑材料一样, -它的品质既影响我们所建造的东西的种类, 也影响我们建造它的方式. +前一章解释了 Lisp 和 Lisp 程序是怎样构建自单一的基本材料: 函数. 与任何建筑材% +料一样, 它的品质既影响我们所建造的东西的种类, 也影响我们建造它的方式.
-本章描述了 Lisp 界流行的那类构造方法. 这些方法的高度精巧允许我们去尝试更加雄心勃勃的那类程序. -下一章将描述一类特别重要的程序, 现在对 Lisp 来说成为可能了: +本章描述了 Lisp 界流行的那类构造方法. 这些方法的高度精巧允许我们去尝试更加雄% +心勃勃的那类程序. 下一章将描述一类特别重要的程序, 现在对 Lisp 来说成为可能了: 程序逐渐进化而不是以老式的先计划再实现的方法开发.
-\section{函数型设计} +\section{函数式设计} \label{sec:functional_design}
+一个对象的特征会被创造它的元素所影响. 例如一个木制建筑和石制建筑看起来是不同% +的. 甚至当你离得很远, 看不清究竟是木头还是石头, 你也可以大体说出它是用什么造% +的. Lisp 程序的特征同样也会受 Lisp 程序的结构所影响.
+\texttt{函数式编程} 意味着通过返回值而不是副作用来写程序. 副作用包括破坏性修% +改对象 (例如通过 \texttt{rplaca}) 以及变量赋值 (例如通过 \texttt{setq}). 如% +果副作用很少并且本地化, 程序就会容易阅读, 测试和调试. Lisp 并不总是写成这种% +风格, 但随着时间的推移, Lisp 和函数式编程之间变得越来越不可分离. + +一个例子可以说明函数式编程和你在其他语言里做的究竟有何不同. 假设某种原因我们% +想把列表里的元素逆序一下. 和写一个函数逆序一个列表相反, 我们写一个函数, 它接% +受一个列表, 然后返回一个带有相同元素但以相反顺序排列的列表. + +图 \ref{fig:bad-reverse} 包含一个对列表求逆的函数. 它把列表看作数组, 按位置% +取反; 它的返回值是不合理的: +\begin{verbatim} +> (setq lst '(a b c)) +(A B C) +> (bad-reverse lst) +NIL +> lst +(C B A) +\end{verbatim} +函数如其名, \texttt{bad-reverse} 距离好的 Lisp 风格差远了. 更进一步, 它还有% +其它丑陋之处: 因为它的正常工作依赖于副作用, 它把调用者完全带离函数式编程的思% +路了. + +\begin{figure} +\begin{verbatim} +(defun bad-reverse (lst) + (let* ((len (length lst)) + (ilimit (truncate (/ len 2)))) + (do ((i 0 (1+ i)) + (j (1- len) (1- j))) + ((>= i ilimit)) + (rotatef (nth i lst) (nth j lst))))) +\end{verbatim} +\caption{\label{fig:bad-reverse}一个对列表求逆的函数} +\end{figure} + +尽管作为典型的反面教材, \texttt{bad-reverse} 好歹有那么一点功绩: 它展示了交% +换两个值的 Common Lisp 习惯用法. \texttt{rotatef} 宏可以轮转任何普通变量的值% +---所谓普通变量是指那些可以作为 \texttt{setf} 第一个参数的变量. 当它只应用于% +两个参数时, 效果就是把它们交换. + +相反, 图 \ref{fig:good-reverse} 显示了一个能返回逆序表的函数. 通过使用 +\texttt{good-reverse}, 我们可以得到逆序的列表作为返回值; 原始列表没有被改动: +\begin{verbatim} +> (setq lst '(a b c) +(A B C) +> (good-reverse lst) +(C B A) +> lst +(A B C) +\end{verbatim} + +\begin{figure} +\begin{verbatim} +(defun good-reverse (lst) + (labels ((rev (lst acc) + (if (null lst) + acc + (rev (cdr lst) (cons (car lst) acc))))) + (rev lst nil))) +\end{verbatim} +\caption{\label{fig:good-reverse}一个返回逆序表的函数} +\end{figure} + +通常认为你可以通过看一个人的头型来判断他的性格. 无论这对于人来说是真是假, 对% +于 Lisp 来说一般是真的. 函数式程序有着和命令式程序不同的外形. 函数式程序的结% +构完全来自与表达式里参数的复合, 并且由于参数是缩进的, 函数式代码看起来在缩进% +方面变动更多. 函数式代码看起来就像页面上的流体\footnote{某页有一个很典型的例% +子}; 命令式代码看起来结实, 顽固, 就像 Basic 语言那样. + +即使从空间上来看, \texttt{bad-} 和 \texttt{good-reverse} 的外形也说明了哪个% +更好. 而且不仅短些, \texttt{good-reverse} 也更加高效: $O(n)$ 而不是 $O(n^2)$. + +我们可以避免写一个 \texttt{reverse} 的麻烦, 因为 Common Lisp 已经提供了内置% +的. 值得简要看一下该函数, 因为它经常引起关于函数式编程的错误观念. 如果 +\texttt{good-reverse} 那样, 内置的 \texttt{reverse} 通过返回值工作, 并不破坏% +它的参数. 但学习 Lisp 的人们往往假定它像 \texttt{bad-reverse} 那样依赖于副作% +用. 如果在程序的某个部分想逆序一个列表, 他们可能写 +\begin{verbatim} +(reverse lst) +\end{verbatim} +然后还很奇怪为什么函数调用没有效果. 事实上, 如果希望从那样一个函数中受到 +\texttt{作用}, 就必须在调用代码里处理我们自己. 就是说, 需要改为写成这样 +\begin{verbatim} +(setq lst (reverse lst)) +\end{verbatim} +调用诸如 \texttt{reverse} 这样的操作符本意就是取返回值的, 而不是副作用. 你自% +己的程序也值得以这种风格写---不仅因为它固有的好处, 而是因为, 如果你不这样写, +就等于在跟语言作对. + +在比较 \texttt{bad-} 和 \texttt{good-reverse} 时我们还忽略了一点, 那就是 +\texttt{bad-reverse} 里没有点对 (cons). 它在原始列表上面操作而不构造新列表. +这样是比较危险的---原始列表可能在程序的其他地方还需要---但为了效率有时可能必% +须这样做. 为满足这种需要, Common Lisp 还提供了一个 $O(n)$ 的称为 +\texttt{nreverse} 的求逆函数的破坏性版本. + +所谓破坏性函数指那类能改变传给它的参数的函数. 即便如此, 破坏性函数也通常通过% +取返回值的方式工作: 你必须假定 \texttt{nreverse} 将会回收利用你作为参数传给% +它的列表, 但不能假设它帮你求逆了那个列表. 和以前一样, 逆序的列表只能通过返回% +值拿到. 你仍然不能把 +\begin{verbatim} +(nreverse lst) +\end{verbatim} +在函数的中间然后假定从那以后 \texttt{lst} 就是逆序的了. 下面的情况在大多数实% +现里都会发生: +\begin{verbatim} +> (setq lst '(a b c)) +(A B C) +> (nreverse lst) +(C B A) +> lst +(A) +\end{verbatim} +要想真正求逆一个列表, 你就不得不把 \texttt{lst} 赋值成返回值, 就和使用 +\texttt{reverse} 一样. + +如果一个函数宣称是破坏性的, 这并不意味着调用这些函数是为了取得副作用. 危险之% +处在于某些破坏性函数给人留下了破坏性的印象. 例如, +\begin{verbatim} +(nconc x y) +\end{verbatim} +总是和 +\begin{verbatim} +(setq x (nconc x y)) +\end{verbatim} +具有相同效果. 如果你写依赖于前一个用法的代码, 某些时候它可以正常工作. 然而当 +x 是\texttt{nil} 的时候它就不是你所期待的行为了. + +只有少数 Lisp 操作符本意就是为了副作用. 一般而言, 内置操作符本来是为了调用后% +取返回值的. 不要被 \texttt{sort}, \texttt{remove}, 或者 \texttt{substitute} +这样的名字所误导. 如果你需要副作用, 对返回值使用 \texttt{setq} 就好. + +这个规则主张某些副作用其实不可避免. 具有函数式的编程思想并不是说不该有副作用. +它只是说除非必要最好不要有. + +养成这个习惯是需要花些时间的. 一个好的开始是尽可能少地使用下列操作符: + +\begin{quote} +\texttt{set setq setf psetf psetq incf decf push pop pushnew +rplaca rplacd rotatef shiftf remf remprop remhash} +\end{quote} + +还包括 \texttt{let*}, 命令式程序经常隐藏其中. 当然这样做也只能起到帮助作用, +并非成为好的 Lisp 风格的准则. 然而, 仅此一项就可让你有所进步了. + +在其他语言里, 导致副作用的最普通原因就是需要函数返回多个值. 如果函数只能返回% +一个值, 那它们就不得不通过改变参数来 ``返回'' 其余的值. 幸运的是在 Common +Lisp 里不必这样做, 因为任何函数都可以返回多值. + +内置函数 \texttt{truncate} 返回两个值, 例如---被截断的整数, 以及被截掉的小数% +部分. 典型的实现当在 toplevel 下调用这个函数时两个值都会返回: +\begin{verbatim} +> (truncate 26.21875) +26 +0.21875 +\end{verbatim} +当调用它的代码只需要一个值时, 第一个值被使用: +\begin{verbatim} +> (= (truncate 26.21875) 26) +T +\end{verbatim} +通过使用 \texttt{multiple-value-bind} 调用方代码可以两个值都捕捉到. 该操作符% +接受一个变量列表, 一个调用, 以及一段程序体. 程序体在变量被绑定到函数调用的相% +应返回值的情况下被求值: +\begin{verbatim} +> (multiple-value-bind (int frac) (truncate 26.21875) + (list int frac)) +(26 0.21875) +\end{verbatim} +最后, 为了返回多值, 我们使用 \texttt{values} 操作符: +\begin{verbatim} +> (defun powers (x) + (values x (sqrt x) (expt x 2))) +POWERS +> (multiple-value-bind (base root square) (powers 4) + (list base root square)) +(4 2.0 16) +\end{verbatim} +一般来说函数式编程是个好主意. 对于 Lisp 来说尤其是这样, 因为 Lisp 很自然地支% +持. 诸如 \texttt{reverse} 和 \texttt{nreverse} 这样的内置操作符的本意就是以% +这种方式被使用的. 其他操作符, 例如 \texttt{values} 和 +\texttt{multiple-value-bind}, 是特别为了使函数式编程更容易才提供的. + +\section{命令式由外而内} +\label{sec:imperative_outside-in} + +函数式编程的目的如果跟那些更普通的观点, 命令式, 想比较起来的话可以显示得更加% +清楚一些. 函数式程序告诉你它想要什么; 命令式程序告诉你它要做什么. 函数式程序% +说 ``返回一个由 \texttt{a} 和 $x$ 的第一个元素的平方所组成的列表:'' +\begin{verbatim} +(defun fun (x) + (list 'a (expt (car x) 2))) +\end{verbatim} +而命令式程序会说 ``取得 $x$ 的第一个元素, 然后把它平方, 然后返回由 \texttt{a} +和那个平方所组成的列表:'' +\begin{verbatim} +(defun imp (x) + (let (y sqr) + (setq y (car x)) + (setq sqr (expt y 2)) + (list 'a sqr))) +\end{verbatim} +Lisp 程序员有幸可以同时用这两种方式来写程序. 某些语言只适合于命令式编程---最% +明显的是 Basic, 以及大多数机器语言. 事实上, \texttt{imp} 的定义和多数 Lisp +编译器从 \texttt{fun} 生成的机器语言代码在形式上很相似. + +既然编译器能为你做为什么还要写这样的代码呢? 对于许多程序员来说, 这甚至不是个% +问题. 语言的模式取决于我们的想法: 一些过去使用函数式语言编程的人可能已经开始% +命令式地思考思考程序了, 并且实际上可能发现编写命令式程序比函数式容易一些. 这% +一思维惯例是值得克服的, 如果有一种语言可以帮助你做到的话. + +对于其他语言的同行来说, 刚开始使用 Lisp 可能像第一次踏入溜冰场那样. 事实上在% +冰上比在干地面上更容易行走---如果使用溜冰鞋的话. 然后你对这项运动的看法就会% +彻底改观. + +溜冰鞋对于冰的作用, 就跟函数式编程对 Lisp 的作用是一样的. 这两样东西在一起允% +许你更优雅地移动, 以更少的努力. 但如果你已经习惯于另一种行走模式, 开始的时候% +你就不能感觉到这一点. 把 Lisp 作为第二语言学习的一个障碍就是学会以函数式风格% +来编程. + +幸运地是, 存在一种把命令式程序转换成函数式程序的诀窍. 开始时你可以把这一诀窍% +应用到已完成的代码里. 不久以后你就可以预计这一过程, 在写代码的同时做转换了. +然后不久, 你从一开始就可以用函数式思想考察你的程序了. + +这个诀窍就是认识到命令式程序其实是一个从里到外翻过来的函数式程序. 要想翻出命% +令式程序中蕴含的函数式的那个, 也只需从外到里翻一下. 让我们在 \texttt{imp} 上% +实验一下这个技术. + +我们首先注意到的是初始 \texttt{let} 里 \texttt{y} 和 \texttt{sqr} 的创建. 这% +就标志着以后不会有好事了. 就像那运行期的 \texttt{eval}, 未初始化的变量很少需% +要以至于被看作程序缺陷的征兆. 这些程序就像针插在程序上, 歪曲了程序的原有形状. + +无论如何, 我们从一开始就忽略掉它们, 然后直接到程序的结尾. 一个命令式程序里最% +后发生的事情, 也就是函数式程序最外层发生的事情. 所以我们的第一步是抓取最终对 +\texttt{list} 的调用并且把程序的其余部分塞到里面去---就好像把一件衬衫从里到% +外翻过来. 我们继续重复做相同的转换, 就好像我们先翻衬衫的套筒, 然后再翻袖口那% +样. + +从结尾处开始, 我们将 \texttt{sqr} 替换成 \texttt{(expt y 2)}, 得到: +\begin{verbatim} +(list 'a (expt y 2)) +\end{verbatim} +然后我们将 \texttt{y} 替换成 \texttt{(car x)}: +\begin{verbatim} +(list 'a (expt (car x) 2)) +\end{verbatim} +现在我们可以把其余代码扔掉了, 已经把所有内容都填到最后一个表达式里了. 在这个% +过程中我们消除了对变量 \texttt{y} 和 \texttt{sqr} 的需要, 也可以把 +\texttt{let} 扔掉了. + +最终的结果比我们开始的时候要短, 而且更加易懂. 在原先的代码里, 我们面对着最终% +的表达式 \texttt{(list 'a sqr)}, 并不能立即清楚 \texttt{sqr} 的值从何而来. +现在返回值的来源则向路标一样呈现在我们眼前了. + +本章的这个例子很短, 但相关的技术是可以任意扩展的. 它对于大型函数来说确实更有% +价值. 即使那些产生副作用的函数, 也可以把其中没有副作用的部分清理得干净一些. + +\section{函数式接口} +\label{sec:functional_interface} + +某些副作用比其他的更坏. 例如, 尽管这个函数调用了 \texttt{nconc} +\begin{verbatim} +(defun qualify (expr) + (nconc (copy-list expr) (list 'maybe))) +\end{verbatim} +但它保留了引用透明.\footnote{关于引用透明的定义见某页} 如果你用一个给定参数% +调用它, 它总是返回相同 (\texttt{equal}) 的值. 从调用者的观点来看, +\texttt{qualify} 就和纯函数型代码一样. 但我们不能对 \texttt{bad-reverse} (第 + \pageref{fig:bad-reverse} 页) 也说同样的话, 那个函数确实修改了它的参数. + +为了不把所有副作用都相等地看作不好的, 如果我们有某种方法能区分这些情况, 那% +将是很有帮助的. 我们可以非正式地说一个函数修改某些其他函数都不拥有的东西是无% +害的. 例如, \texttt{qualify} 里的 \texttt{nvonc} 就是无害的, 因为作为第一个% +参数的列表是新生成的. 没其他函数拥有它. + +在通常情况下, 我们在谈论拥有者关系时并不说被哪个函数, 而必须说被哪个函数的 +invocation. 尽管这里并没有其他函数拥有变量 x, +\begin{verbatim} +(let ((x 0)) + (defun total (y) + (incf x y))) +\end{verbatim} +但一次调用的效果会在接下来的调用中看到. 所以规则应当是: 一个给定的 invocation +可以安全地修改它唯一拥有的东西. + +究竟谁拥有参数和返回值? Lisp 里的惯例看起来是 invocation 拥有作为返回值收到% +的对象, 但并不拥有那些传给它作为参数的对象. 凡是修改它们参数的函数都应该用 +``破坏性的'' 标签区分开, 但对于那些修改了返回给它们的对象的那些函数就没有特% +别的分类了. + +接下来这个函数附议了上述说法, 例如: +\begin{verbatim} +(defun ok (x) + (nconc (list 'a x) (list 'c))) +\end{verbatim} +它调用了 \texttt{nconc}, 由于被 \texttt{nconc} 拼接的列表总是新建的而非% +传给 \texttt{ok} 作为参数的那个列表, 所以 \texttt{ok} 本身也是好的. + +如果稍微写得不同一点儿, 例如: +\begin{verbatim} +(defun not-ok (x) + (nconc (list 'a) x (list 'c))) +\end{verbatim} +那么对 \texttt{nconc} 的调用就会修改传给 \texttt{not-ok} 的参数了. + +许多 Lisp 程序违背了这一惯例, 至少在局部是这样. 尽管如此, 正如我们说 +\texttt{ok} 的那样, 局部违背不会降低主调函数的资格. 而且那些遇到前述情况的函% +数仍然会保留很多纯函数式代码的优点. + +要想写出真的跟函数式代码不可区分的程序, 我们还要再增加一个条件. 函数不能和其% +他不遵守这些规则的代码共享对象. 例如, 尽管这个函数没有副作用, +\begin{verbatim} +(defun anything (x) + (+ x *anything*)) +\end{verbatim} +但它的返回值依赖于全局变量 \texttt{*anything*}. 这样如果任何其他函数可以改变% +这个变量的值, \texttt{anything} 就可以返回任何东西. + +把代码写成让每个 incovation 只修改它自己拥有的东西, 几乎和纯函数型代码一样好% +了. 一个满足前述所有条件的函数至少对外界看来具有一个函数式接口: 如果你用同一% +个参数调用它两次, 你应当得到同样的结果. 并且这也是, 正如下一章所展示的那样, +自下而上编程的最重要组成部分. + +破坏性操作符的一个问题是, 就像全局变量那样, 它们将破坏程序的局部性. 当你写函% +数式代码时, 你可以缩小注意力: 你只需考虑这个函数调用的那些函数, 被哪些函数调% +用, 以及你正在写的这个函数. 当你想要破坏性地修改某些东西时这个好处就消失了. +它可能被任何地方用到. + +上面的条件不能保证你得到和纯粹的函数式代码一样的局部性, 尽管它们确实在某种程% +度上有所改进. 例如, 假设 \texttt{f} 调用了 \texttt{g}, 如下: +\begin{verbatim} +(defun f (x) + (let ((val (g x))) + ; safe to modify val here? + )) +\end{verbatim} +在 \texttt{f} 里把某些东西 nconc 到 \texttt{val} 上面安全吗? 如果 \texttt{g} +是 \texttt{identity} 的话就不安全: 这样我们就修改了某些原本作为参数传给 +\texttt{f} 本身的东西. + +所以即使在那些确实遵守了规定的程序里, 在我们想要修改某些东西时, 也不得不看看 +\texttt{f} 之外的东西. 尽管如此, 也不需要看得太远: 与其担心整个程序, 我们现% +在只需考虑从 \texttt{f} 开始的那个子树. + +一个推论是函数不应当返回任何不能安全修改的东西. 这样的话, 就应当避免写那些返% +回包含引用对象的值. 如果我们定义 \texttt{exclaim} 使它的返回值包含一个引用列% +表, +\begin{verbatim} +(defun exclaim (expression) + (append expression '(oh my))) +\end{verbatim} +那么任何后续的对返回值的破坏性修改 +\begin{verbatim} +> (exclaim '(lions and tigers and bears)) +(LIONS AND TIGERS AND BEARS OH MY) +> (nconc * '(goodness)) +(LIONS AND TIGERS AND BEARS OH MY GOODNESS) +\end{verbatim} +将替换函数里的列表: +\begin{verbatim} +> (exclaim '(fixnums and bignums and floats)) +(FIXNUMS AND BIGNUMS AND FLOATS OH MY GOODNESS) +\end{verbatim} +为使 \texttt{exclaim} 避免这个问题, 它应该被写成: +\begin{verbatim} +(defun exclaim (expression) + (append expression (list 'oh 'my))) +\end{verbatim} + +对于函数不应返回引用列表这一规则有一个主要的例外: 那些生成宏展开的函数. +宏展开器可以安全地在它们的展开式里包含引用的列表, 如果这些展开式是直接送到编% +译器那里的. + +其他方面, 对于引用的列表应当总是持怀疑态度. 它们的许多其他用法像是某些原本就% +应当用诸如 \texttt{in} (某页) 这样的宏来完成的. + +\section{交互式编程} +\label{sec:interactive_programming} + +前一章展示了作为一种组织程序的良好方式的函数式风格. 但它的好处还有更多. Lisp +程序员并非完全从美感考虑才采纳函数式风格的. 他们使用他因为它使他们的工作更简% +单. 在 Lisp 的动态环境里, 函数式程序可以不同寻常的速度写成, 并且同时, 通常是% +可信赖的. + +在 Lisp 里调试程序相对简单. 很多信息在运行期是可见的, 可以帮助跟踪错误的原因. +但更重要的是你可以轻易地 \textsl{测试} 程序. 你不需要编译一个程序然后一次性% +测试所有东西. 你可以通过 toplevel 循环通过单独地调用每个函数来测试它们. + +增量测试是如此有价值以至于 Lisp 风格已经进化到可以充分利用它. 用函数式风格写% +出的程序可以每次一个函数地去理解它, 从读者的观点来看这是它的主要优点. 而且, +函数式风格也被完美地采纳用于增量测试: 以这种风格写出的程序可以每次一个函数地% +进行测试. 当一个函数既不检查也不改变外部状态时, 任何 bug 都能立即被发现. +这样一个函数影响外面世界的唯一渠道是通过其返回值, 在你能想到的范围内, 你可以% +完全相信那些产生它们的代码. + +事实上有经验的 Lisp 程序员会尽可能设计它们的程序以利于测试: +\begin{enumerate} +\item + 他们试图把副作用分离到少量函数里, 以便程序中更多的部分可以写成纯函数式风格. +\item 如果一个函数必须产生副作用, 他们至少试图给它一个函数式的接口. +\item 他们给每个函数一个单一的, 很好定义了的目的. +\end{enumerate} +当一个函数被写成, 他们可以用一组有代表性的情况进行测试, 然后再转向下一个. 如% +果每一块转都干它该干的事, 那么墙就不会倒. + +在 Lisp 里, 墙可以得到更好的设计. 想象那种跟某人的距离远到有一分钟传输延迟的% +对话. 现在想象跟隔壁房间里某人说话. 你将不只是得到一个同样但是更快的对话, 而% +是将得到一个完全不同类型的对话. 在 Lisp 中, 开发软件就像是面对面交谈. 你可以% +边写代码边做测试.
%%% Local Variables: %%% coding: utf-8
Added: books/onlisp/Makefile ============================================================================== --- (empty file) +++ books/onlisp/Makefile Wed Sep 5 12:50:59 2007 @@ -0,0 +1,11 @@ +all: onlisp.pdf onlisp.ps + +onlisp.pdf: onlisp.dvi + dvipdfmx onlisp.dvi + +onlisp.ps: onlisp.dvi + dvips onlisp.dvi + +onlisp.dvi: *.tex + latex onlisp.tex + latex onlisp.tex
Modified: books/onlisp/onlisp.tex ============================================================================== --- books/onlisp/onlisp.tex (original) +++ books/onlisp/onlisp.tex Wed Sep 5 12:50:59 2007 @@ -2,6 +2,7 @@ \usepackage{CJK} \usepackage{indentfirst} \usepackage{amsmath} +%\usepackage{hyperref}
\begin{document} \begin{CJK}{UTF8}{song}
cl-net-snmp-cvs@common-lisp.net