2011年4月15日星期五

鼓励表达

原文2011年03月28日写在自己的QQ日志上,标题与内容无关,现在备份与此。
当时写文的目的是想表达缩手缩脚不到代表能避免失败,意思是说要有积极的行动吧。

这篇的话题是关于向别人介绍东西的模式。

这个话题首先来说介绍东西,其目的是自己向别人传递信息。其中所要达到的效果是自己把要表达的意思传递出于,而同时别人也能有效准确的接收并认同。

这个话题的另一方面,或者说是此篇的重点和目的所在——模式。

对于做一件事情,例如这里说的介绍东西这件事情。我们可以说要认真努力地去做,并说是克服惰性不让自己偷懒什么的。但是这样的问题在于,一些主观的情感上的态度并不能代表对处理一件事情的经验,而且对于特定的事情经验会比热情要稳定可靠。另一方面,一味强调精神和品格,会为行事过程中带来很多压力。

模式(Pattern),是针对一类事情在处理方式上的共同点,是处理相似的不同的问题时可复用的结构。例如这里的问题是介绍东西,而模式就是一些向别人介绍东西的方式和方法。通过使用模式,可以获得前人处理问题的经验,也总结了自身的经验,使得问题可以处理的越来越好。同时,依照模式来处理问题,简化了问题的分析思考,使得问题的处理有了一些最基本的质量保障。

具体的说就是。模式的作用体现在两个方面:
1.保障。通过遵循已有的模式的约束,可以在有限的行为下获得充足的效果。
2.复用。将处理问题的经验总结为模式,以便之后使用和遵循。
比如说一些文书有特定的格式要求。依照这个格式来写就是给文字的展开提供了直接的思路,同时在满足了格式的同时也保证需要的内容都有提及。

也就是说在处理问题中运用模式,对于减少负担、提升质量都是有益处的。

那么这里回到在这一篇里所说的模式——向别人介绍东西,下面的内容将试图为此类模式提供一个示例:

首先可以向别人描述整个介绍行为的“动机”,也是是说为什么要介绍,为什么要介绍给对方,为什么由自己来介绍等等问题。在介绍动机的时候,可以是从历史的角度来说,介绍事情相关的背景。或者展示情景和问题,而让介绍的内容成为对应的一种解答。在对动机的描述中,重点还没有切换到所要介绍的事物具体是什么,而目的是为接下来的介绍行为提供一个连贯的思路和线索。

接下来可以展示一个简单却完整的“示例”,即使对方不理解也没有关系。这里的目的不是让对方完全接受所介绍的内容,只需要对所给的例子有整体的印象便可。在这么早的时候给实例的目的,是想给对方一个具体的全局的观感。对方在知道实例后,便能够去尝试应用示例,便可判断自己是否已经迈出了第一步。通过先给的实例,也让介绍者先试图以简化的视角来开始整个介绍。虽然事情会有它自身的复杂性,但是介绍过程是为了简化事情被介绍的复杂性而进行的,而不是出于复杂性给介绍者的满足感。同时,这个实例也是为引出更具体的描述和更多方面的展开。

现在便到了整个介绍过程中占据时间最长的部分,也就是介绍内容的主体部分,对要说的内容的“展开”。这里原则是尽量减少这部分带给人的意外感。把所要说的话题和描述话题的方式都尽量在之前给实例时候都简要的提及,这部分只是对先前给过的思路的展开。在这一部分中对信息处理的主动权是交给接收者的,而对方依据自己已经描述过的思路来获取信息。而作为介绍者而言在这一部分所要做的是对提出的问题进行分析,并将新的要点在分析中展示出来。从外表上看这一部分会和前面的那部分有所重复,这种重复感是让别人接受的一种方式。并且让这个分析解释部分显得冗长的时候,反而会给对方带来期待感。

从某种意义上说,下面要说的这部分占的时间不是很长,却是会给接收者带来最深刻印象的一部分——这就是对对所介绍内容进行的“抽象”。上面详细的分析说明的那一步,虽然占时间最多,但是信息传递的主体是在接受者上,那么现在这一步就又将主体转回到说明者上了。让说明者来强调所介绍的是什么,而不是像先前的分析过程重在思路的展开上。在这一步里,可以借助一些名词来概括,或者提供一些用来回忆再现和相互交流的手段。这是也可以推荐一些手册供对方查阅。

现在到了最后的一步,作为介绍者而言需要给对方的反应以“回应”。给予对方正确的判断以肯定,同时双方自由交流一些态度和看法,并留给对方展示的余地。而不是让介绍过程以一种反正自己尽责了的心态来进行。

此外,在介绍过程中可以结合图表实例来获得更好的演示效果。

那么,我这里所说的一种模式便介绍完了。这里的动机并不是什么应该怎么做或者可以怎么做,以上内容的选取并不是从实际的经验的出发的。因为这里介绍者的身份很容易联想到教师或者作者之类的身份。而其实呢,向别人介绍一些事情是人与人之间总在发生的事情。

以上所写的内容的出发点,是想展示一下“模式”带来的审美观念。假如处理问题是从模式出发的话,可以较少负担,并获得有保障的效果。从这样一个角度来想问题还是很有趣的。

就像是说,对于先前做过的事情,去向“如果当时……就能……”。这样的想法是对以后做同样的事情提供经验,而并不能保证先前的事情一定就有最好的结果。如果想先前一点可遗憾的东西也没有的话,那么一个逻辑上说的通的选择是——什么都不要做。

好了,本文的内容也就是尽量逻辑通顺而已,什么模式也没去用。

2011年4月14日星期四

用OCaml的进行形式文法解析

本来是想写这篇的,因为OCaml确实在这一个领域很出色的语言。虽然ML代码结构看上去比C语言要清晰易读许多,不过说实在的自己的大脑还是实在没养成按照写公式一样的写出代码来。结果就是在FP中还是用了已经比较熟悉的Scheme进行试验了,感觉写Scheme就像玩橡皮泥,在拿捏和拼装之后,一个原本还较陌生的逻辑就写出来了。这里的意思是说对于我这篇所要达到的目的,还是用Scheme给先写出来的。这不是关于语言特性的,而是关于思维的波长正好和问题一致而已。如果一定要牵涉到语言层面的影响的话,只能说我是在问题和语言都初次为一个情景而组合的,暂时还达不到追求美感的境界。就像听音乐,不管音乐是好好听,音质高就会很有震撼了。

于是本文成了草稿的回收了,那个另一个版本的半成品(至少是半成品,有个完整的结构也好啊)写在别出了,过段时间在整理过来。当然,不要指望这些文字里有什么东西,个人日志什么的(至少像我这里的几篇)写起来还是很随意的。

这里所用的示例是最简单的整数四则运算表达式的处理,目的包括求值和转化为前缀形式的语法树。



在OCaml手册中有对ocamllex/ocamlyacc的描述,并给出了这样一个例子。虽然写法上还是是依照lex/yacc的,不过鉴于OCaml的语言风格所以看起来似乎要直观一些。

于是这里就直接大篇幅的引用过来了。

http://caml.inria.fr/pub/docs/manual-ocaml/manual026.html
12.6 A complete example
The all-time favorite: a desk calculator. This program reads arithmetic expressions on standard input, one per line, and prints their values.

Here is the grammar definition:
/* File parser.mly */
%token INT
%token PLUS MINUS TIMES DIV
%token LPAREN RPAREN
%token EOL
%left PLUS MINUS /* lowest precedence */
%left TIMES DIV /* medium precedence */
%nonassoc UMINUS /* highest precedence */
%start main /* the entry point */
%type main
%%
main:
expr EOL { $1 }
;
expr:
INT { $1 }
| LPAREN expr RPAREN { $2 }
| expr PLUS expr { $1 + $3 }
| expr MINUS expr { $1 - $3 }
| expr TIMES expr { $1 * $3 }
| expr DIV expr { $1 / $3 }
| MINUS expr %prec UMINUS { - $2 }
;

Here is the definition for the corresponding lexer:
(* File lexer.mll *)
{
open Parser (* The type token is defined in parser.mli *)
exception Eof
}
rule token = parse
[' ' '\t'] { token lexbuf } (* skip blanks *)
| ['\n' ] { EOL }
| ['0'-'9']+ as lxm { INT(int_of_string lxm) }
| '+' { PLUS }
| '-' { MINUS }
| '*' { TIMES }
| '/' { DIV }
| '(' { LPAREN }
| ')' { RPAREN }
| eof { raise Eof }
Here is the main program, that combines the parser with the lexer:

(* File calc.ml *)
let _ =
try
let lexbuf = Lexing.from_channel stdin in
while true do
let result = Parser.main Lexer.token lexbuf in
print_int result; print_newline(); flush stdout
done
with Lexer.Eof ->
exit 0

To compile everything, execute:
ocamllex lexer.mll # generates lexer.ml
ocamlyacc parser.mly # generates parser.ml and parser.mli
ocamlc -c parser.mli
ocamlc -c lexer.ml
ocamlc -c parser.ml
ocamlc -c calc.ml
ocamlc -o calc lexer.cmo parser.cmo calc.cmo

这里把四种运算符都写在定义层次的产生式上了,然后额外的指定了结合顺序和优先级。



在OCaml中另一个用于功能类似的模块是CamlP4,作为库它基于Stream类型提供了用于词法与语法解析的功能,并同时自身也在OCaml中充当了预处理的角色。

递归下降的形式可以用camlp4可以写为这样的形式:
http://www.codecodex.com/wiki/index.php?title=Recursive_descent_parsing
(* File parser.ml *)
let rec factor = parser
| [< ''('; e = expr; '')' >] -> e
| [< 'c >] -> int_of_string (String.make 1 c)
and term = parser
| [< e1 = factor; e = term_aux e1 >] -> e
and term_aux e1 = parser
| [< ''*'; e2 = term >] -> e1 * e2
| [< ''/'; e2 = term >] -> e1 / e2
| [< >] -> e1
and expr = parser
| [< e1 = term; e = expr_aux e1 >] -> e
and expr_aux e1 = parser
| [< ''+'; e2 = expr >] -> e1 + e2
| [< ''-'; e2 = expr >] -> e1 - e2
| [< >] -> e1
let rec statement = parser
| [< e = expr; '';'; ''.'; stream >] ->
Printf.printf "= %d\n%!" e;
statement stream
let rec read() = match input_char stdin with
| ' ' | '\t' | '\n' -> [< read() >]
| c -> [< 'c; read() >]
let () = statement (read())

然后执行如下的编译命令
ocamlc -g -dtypes -pp camlp4o parser.ml -o parser

其原理仍然是基本的递归下降文法解析,特别的地方在于parser是对stream进行的模式匹配和处理,使用语言糖使得函数的定义和生成式的书写有申明式编程的感觉。其他语言如Haskell里也有类似的模块存在,语法糖长相的差异不是问题的说。



关于递归下降语法解析,编译原理方面的书(TC++PL里怎么也会有呢?)都会给出示例的,比如像下面这个:
http://en.wikipedia.org/wiki/Recursive_descent_parser
What follows is an implementation of a recursive descent parser for the above language in C. The parser reads in source code, and exits with an error message if the code fails to parse, exiting silently if the code parses correctly.
Notice how closely the predictive parser below mirrors the grammar above. There is a procedure for each nonterminal in the grammar. Parsing descends in a top-down manner, until the final nonterminal has been processed. The program fragment depends on a global variable, sym, which contains the next symbol from the input, and the function getsym, which updates sym when called.
The implementations of the functions getsym and error are omitted for simplicity.

typedef enum {ident, number, lparen, rparen, times, slash, plus,
minus, eql, neq, lss, leq, gtr, geq, callsym, beginsym, semicolon,
endsym, ifsym, whilesym, becomes, thensym, dosym, constsym, comma,
varsym, procsym, period, oddsym} Symbol;
Symbol sym;
void getsym(void);
void error(const char msg[]);
void expression(void);
int accept(Symbol s) {
if (sym == s) {
getsym();
return 1;
}
return 0;
}
int expect(Symbol s) {
if (accept(s))
return 1;
error("expect: unexpected symbol");
return 0;
}
void factor(void) {
if (accept(ident)) {
;
} else if (accept(number)) {
;
} else if (accept(lparen)) {
expression();
expect(rparen);
} else {
error("factor: syntax error");
getsym();
}
}
void term(void) {
factor();
while (sym == times || sym == slash) {
getsym();
factor();
}
}
void expression(void) {
if (sym == plus || sym == minus)
getsym();
term();
while (sym == plus || sym == minus) {
getsym();
term();
}
}
void condition(void) {
if (accept(oddsym)) {
expression();
} else {
expression();
if (sym == eql || sym == neq || sym == lss || sym == leq || sym == gtr || sym == geq) {
getsym();
expression();
} else {
error("condition: invalid operator");
getsym();
}
}
}
void statement(void) {
if (accept(ident)) {
expect(becomes);
expression();
} else if (accept(callsym)) {
expect(ident);
} else if (accept(beginsym)) {
do {
statement();
} while (accept(semicolon));
expect(endsym);
} else if (accept(ifsym)) {
condition();
expect(thensym);
statement();
} else if (accept(whilesym)) {
condition();
expect(dosym);
statement();
}
}
void block(void) {
if (accept(constsym)) {
do {
expect(ident);
expect(eql);
expect(number);
} while (accept(comma));
expect(semicolon);
}
if (accept(varsym)) {
do {
expect(ident);
} while (accept(comma));
expect(semicolon);
}
while (accept(procsym)) {
expect(ident);
expect(semicolon);
block();
expect(semicolon);
}
statement();
}
void program(void) {
getsym();
block();
expect(period);
}

在这个解析过程中,是根据下一位的单词进行解析状态的判断的。
比如说在读到1+1时,下一位是+(1+1+)或者*(1+1*)就要按照不同的语法树解析。


关于OCaml做这方面应用的还有一个优势是ADT(抽象数据类),很方便用来描述类型的并级。
并且OCaml的纯静态类型是在编译时擦除变量类型的,这也一个简单且基础的类型机制。
此外将虽然句法解析的能力包括了词法解析,但是将两者分开来的话能都使得实现变得简单。
比如整数运算到浮点数的扩充,就可以放在词法解析的过程中先识别。
上面的示例还有一些可以扩充的方面,比如顺便增加一下条件语句和赋值语句什么的。

那么就顺便说下OCaml中类型的复合,有:
Tuple/Recode —— int*char,{num: int; denum: int};
Enum/Union —— Int|Char,Int of int;
Variant Type —— 'a t;
Module/Class —— 有点像C++中的模板
Signature —— 这个是作为参数使用的类型
其中类型用tpye定义,并允许定义中递归。
此外的arrary和list类型可以看作已有复合方式的再一次复合,虽然说会单独被实现的。
使用ADT的好处是在解析文本后生成的节点都用类型的Union来表示,
至于OCaml的特有特性Polymorphic variants则可让系统自行定义类型的联合。


最后强调一点,一定要看到下面这句话哦,不然出什么问题就会麻烦了
——以上的内容仅娱乐目的,不保证内容一定正确哦。
http://rosettacode.org/wiki/Y_combinator