能伸亦能屈,但不脱离中庸之道
用 nikola 写静态博客

m5 v0.1 使用帮助

rca posted @ 2013年2月03日 23:43 in 伪程序猿的行为艺术 with tags GNU M4 m5 文学编程 , 2188 阅读

大家好,m5 v0.1 非隆重发布,有个人在大雪封山弹尽粮绝的状况下百无聊赖的写了这篇指南,无意指引他人如何使用这个工具,只是想表明这个世界上至少还有一个人有可能使用它,他担心日后使用时忘记了如何用。

0. 代码与文档

如果你是程序猿,你肯定知道自己的工作不是只负责敲代码,为了让他人或者很长时间以后的自己更容易理解现在你所敲的代码,你需要很严肃的敲出足够详细的文档来解释这些代码要解决什么问题,采用了什么方法,取得了什么结果。

实际上,文档所表达的思想是在你敲代码之前便已产生,可能有许多人是在这种思想的支配下先写出代码,然后再根据代码去写文档,至少我经常这么干。可能还有许多的人一旦写出了代码,需要来世才可以看到他们写文档。

还有一种观点认为:代码即文档。持这一观点的人觉得他们所写的代码是自解释性的,这样来世也看不到他们写文档。我觉得对于这些人最好的惩罚就是他们在购买到任何产品时,我们应当慈悲的帮助他们烧掉产品使用说明书。

不管之前你是什么心态,从现在开始我们下面所有的讨论都是建立在代码与文档同等重要的基础之上的。

代码与文档的关系无非就是要么是文档嵌入于代码中,要么是代码嵌入于文档中。或许也有第三种关系,即文档与代码分离;存在这种关系的程序也许就类似于人类中的精神分裂者。

将文档嵌入代码中是目前最为常见的方式。最一般的行为是为代码写注释,高级一些的行为是借助类似 Doxygen 这样的工具从代码中抽取注释并将其排版为较为适合人类阅读的文档。这种方式最大的问题就是文档的结构是由代码的结构决定的,而代码的结构是由它的编译/解释器所决定的,这种决定与被决定的关系还可以一直递推到图灵机。比方说,我们写一个 C 程序,一上来就从 main 函数开始写代码可不可以?当然不可以,你至少先需要对 main 函数调用的那些函数进行声明,而我们在阅读代码的时候,往往会先从 main 函数开始。如果给你一本书,这本书的概述部分在书的末尾,你会不会觉得这本书很反人类?

将代码嵌入文档,它有另外一个很奇葩的名字——文学编程(Literate Programming),这种方式很少见也很常见。少见是因为程序猿在工作中很少这么干,常见是因为大部分讲计算机编程的书是这么干的。事实证明,愿意通过阅读这些书籍来学习编程、某个开发框架或者某个应用程序的人要远远多于直接去阅读那些嵌入了文档的代码的人。不妨自检一下,你是愿意先看看《深入理解 Linux 内核》然后再看内核代码,还是直接看内核代码?

将代码嵌入于文档的方式是值得推荐的,而且它完全兼容将文档嵌入于代码这种方式,只需将文档按照代码的逻辑撰写即可,但是后者却很难像前者那样可以自由的选择书写逻辑。

1. m5 是什么

将代码嵌入于文档之后,改善的是文档的可读性,但是我们最终还是需要向编译/解释器提供正确的代码,这就引发了这样的问题:如何从文档中提取散落各处的代码片段并将其合并为编译/解释器所认可的完整的代码?这就是 m5 要努力解决的问题。

在计算机编程的原始社会时代,大神 Knuth 创造了 tangle 工具,用它从他的文档中提取 PASCAL 代码。m5 类似 tangle,但是它的目标是可以提取任何编程语言的代码,前提是从 UTF-8 编码格式的文本文档中提取。如果你打算从 MS Word 文档或 LibreOffice 文档中提取代码,那么 m5 立刻掩面泪奔。

当然,如果期望 m5 提取散布于文档各处的代码片段并正确的将它们合并为完整的代码,你需要对代码片段出现的位置进行设定。你要做的是为各个代码片段取定名称,并按照程序的逻辑安排这些名称的出场次序,然后 m5 便可以按照这样的次序输出程序代码。这就像讲故事,虽然你可以选择顺叙、倒叙、插叙或者乱叙等叙事手法,但是你要讲的故事的内在逻辑不能乱。

2. 获取与使用 m5

m5 项目主页:https://github.com/liyanrui/m5

目前无二进制版,需要从项目仓库迁出代码并自行编译安装,参考以下命令:

$ git clone https://github.com/liyanrui/m5.git
$ cd m5
$ ./configure
$ make
$ sudo make install

注意,m5 依赖 glib 库以及 GNU m4。

m5 需要配合 m4 使用(见 `m5 与 m4 的关系'),用法如下:

$ m5 文学编程文档 | m4 > 程序代码文档
 
例如:
$ m5 test.m5 | m4 > test.c

也支持管道:
$ cat test.m5 | m5 | m4 > test.c

下面对 m5 所能识别的各种标记的用法进行详细的介绍。

2.1 文档格式

既然是将代码嵌入文档,那么你需要为文档的表示选择一种媒介,这种媒介必须是纯文本格式的文档,例如普通的文本文档、HTML、TeX、Markdown、DocBook 等格式。

总之,只要你选择的是 UTF-8 编码的文本文档,m5 不会关心你选择的是哪种文档格式。后面的示例,使用的是普通的文本文档。

2.2 文档内容与代码片段

回忆一下,我们将文档嵌入代码时,文档内容由注释符号囊括,使得它与代码有着明确的界限。在 m5 看来,将代码嵌入文档也是如此,二者不可以随意混杂不分彼此,而是泾渭分明。

本质上,将文档嵌入代码,是以代码为主,文档内容为辅,而将代码嵌入文档,则是以文档内容为主,代码为辅。前者是用文档的内容诠释与之对应的代码片段的含义,后者则是以代码帮助读者更加具体的理解作者的思想。不过,这些只是我一厢情愿的想法,具体该如何执行取决于自己的意愿。前面也曾提到,即使是将代码嵌入文档,你也完全有可能沦落为将文档嵌入代码的境界。

m5 是使用 `\_{...} +=' 这样的标记表示一份代码片段的开始,以 `@>' 标记表示这份代码片段的结束。`\_' 符号的寓意是你要下滑到某种图灵机的世界中了。`@>' 符号的寓意是你要从这个弯弯曲曲难以理解的图灵机世界中返回人间。在这两个标记的上文和下文要么是其他代码片段,要么是文档内容。

2.3 为代码片段取名字

表征代码片段开始的标记 `\_{...} +=',将花括号中的省略号替换为一个字符串,便是为代码片段命名。例如:

\_{say_hello 函数的定义} +=
void
say_hello (void)
{
        printf ("Hello World!\n");
}
@>

再例如:

\_{从当前结点中选择距 entry 最近的子结点作为子树} +=
gdouble d_min = G_MAXDOUBLE;
for (GNode *iter = current_node->next; iter != NULL; iter = iter->next) {
        gdouble d = pm_box_dist ((PmBox *)(entry->data), (PmBox *)(iter->data));
        if (d_min > d) {
                subtree = iter;
                d_min = d;
        }
}
@>

除了不可以使用与代码片段标记雷同的字符之外,代码片段的名字没有特殊的要求。

2.4 同名的代码片段

m5 会将同名的代码片段合并到一起。例如:

\_{蜀道难} +=
蜀道之难,难于上青天。
@>

\_{蜀道难} +=
蚕丛及鱼凫,开国何茫然!尔来四万八千岁,不与秦塞通人烟。
@>

m5 会按照 `蜀道难' 这个代码片段出现的先后次序进行合并,得到:

\_{蜀道难} +=
蜀道之难,难于上青天。
蚕丛及鱼凫,开国何茫然!尔来四万八千岁,不与秦塞通人烟。
@>

这种合并称为代码片段的尾部追加。

允许同名代码片段的追加,这意味着你可以将较长且包含多个子片段的代码片段进行分解并嵌入于文档各处。

除了尾部追加之外,还有一种首部追加。例如:

\_{蜀道难} +=
蜀道之难,难于上青天。
@>

\_{蜀道难} +=
蚕丛及鱼凫,开国何茫然!尔来四万八千岁,不与秦塞通人烟。
@>

\_^{蜀道难} +=
噫吁嚱!危乎高哉!
@>

注意第三份代码片段,在 `\_' 符号后有一个 `^' 符号,这表示将当前的代码片段插入到前面出现的所有 `蜀道难' 代码片段的合并结果的起始位置。上例被 m5 合并的最终结果为:

\_{蜀道难} +=
噫吁嚱!危乎高哉!
蜀道之难,难于上青天。
蚕丛及鱼凫,开国何茫然!尔来四万八千岁,不与秦塞通人烟。
@>

2.5 代码片段的嵌套

对于一个代码片段 A,如果想将它插入到代码片段 B 中,只需在代码片段中插入 A 的名称即可。

例如,下面这个代码片段可视为 A。

\_{收集 zipper 所指结点的子结点包围盒} +=
PmList *boxes = pm_list_alloc ();
g_node_children_foreach (zipper,
                         G_TRAVERSE_ALL,
                         pm_rc_tree_collect_boxes,
                         boxes);
PmBox *box = pm_box_merge (boxes);
pm_list_free (boxes);
@>

下面的代码片段视为 B,在其中插入 A 的名称即表示将 A 插入于 B。

\_{调整 zipper 所指结点的包围盒} +=
收集 zipper 所指结点的子结点包围盒
if (pm_box_is_equal (((PmBox *)(zipper->data)->data), box)) {
        break;
} else {
        box->data = ((PmBox *)(zipper->data))->data;
        pm_box_free (zipper->data);
        zipper->data = box;
}
@>

2.6 主代码片段

就像大部分计算机编程语言都会定义程序的入口,例如 C 语言的入口就是名字为 main 的函数。m5 也规定了代码片段的入口,这个入口用于告诉 m5 此处是提取代码片段的起点。一旦 m5 获得提取代码片段的起点,它便会从这一起点开始,首先获取这个起点的代码片段,然后再对嵌入其中的其他代码片段的嵌套进行追踪提取并插入于主代码片段相应的位置。

主代码片段的名字是一个星号 `*'。例如:

\_{*} +=
static void
pm_rc_tree_adjust_boxes (PmRCTree *tree, GNode *source)
{
        GNode *zipper = source;
        while (zipper) {
                调整 zipper 所指结点的包围盒
                zipper = zipper->parent;
        }
}
@>

m5 找到主代码片段后,它便从中提取以下代码:

static void
pm_rc_tree_adjust_boxes (PmRCTree *tree, GNode *source)
{
        GNode *zipper = source;
        while (zipper) {
                调整 zipper 所指结点的包围盒
                zipper = zipper->parent;
        }
}

然后它会发现代码中嵌入了一份名为 `调整 zipper 所指结点的包围盒' 的代码片段,它便追踪提取 2.5 节中所给出的的代码片段,然后将其插入于主代码片段对应位置中,于是主代码片段变为:

static void
pm_rc_tree_adjust_boxes (PmRCTree *tree, GNode *source)
{
        GNode *zipper = source;
        while (zipper) {
                收集 zipper 所指结点的子结点包围盒
                if (pm_box_is_equal (((PmBox *)(zipper->data)->data), box)) {
                        break;
                } else {
                        box->data = ((PmBox *)(zipper->data))->data;
                        pm_box_free (zipper->data);
                        zipper->data = box;
                }
                zipper = zipper->parent;
        }
}

然后 m5 又发现代码片段中其中还有一个名为 `收集 zipper 所指结点的子结点包围盒' 的代码片段(见 2.5 节),它便继续跟踪提取并合并至主代码片段中,得到:

static void
pm_rc_tree_adjust_boxes (PmRCTree *tree, GNode *source)
{
        GNode *zipper = source;
        while (zipper) {
                PmList *boxes = pm_list_alloc ();
                g_node_children_foreach (zipper,
                                         G_TRAVERSE_ALL,
                                         pm_rc_tree_collect_boxes,
                                         boxes);
                PmBox *box = pm_box_merge (boxes);
                pm_list_free (boxes);
                if (pm_box_is_equal (((PmBox *)(zipper->data)->data), box)) {
                        break;
                } else {
                        box->data = ((PmBox *)(zipper->data))->data;
                        pm_box_free (zipper->data);
                        zipper->data = box;
                }
                zipper = zipper->parent;
        }
}

至此,由主代码片段为起点的全部代码片段的提取与合并结束。

2.7 字符的转义

文档中如果出现于 m5 前面所述的标记需要对其进行转义,否则会将 m5 弄的乱七八糟。

转义的方法是将要转义的字符串置于 `@{'  与 `@}' 标记之间,例如要对代码片段结束标记 `@>' 进行转义,需要写为:

@{@>@}

注意:如果要对 `@{'  与 `@}' 这两个标记本身进行转义的话,也会将 m5 弄的乱七八糟。

3. m5 与 m4 的关系

实际上,m5 所作的主要工作是对代码片段进行合并,而代码片段的追踪提取是依赖于 m4 的宏展开机制实现的。换句话说,m5 只是将文档解析为 m4 代码,剩下的工作由 m4 接管。

如果你不知道 m4 是什么怪物,请阅读维基百科上的 m4 词条

Avatar_small
rca 说:
2013年2月05日 11:26

@views63: 不予回应

冷水 说:
2013年2月20日 10:25

谢谢
看起来这玩意对于写笔记顺面写出代码来挺有用的

不知道真正实施起来有什么问题

Avatar_small
冷水 说:
2013年3月06日 17:23

因为m4的问题
代码里面不能有单引号
因此matlab fortran 没法用
注释里面也不能写单引号

Avatar_small
rca 说:
2013年3月08日 01:19

@冷水: m4 提供了使用别的符号取代引号的宏,只是 m5 没有把这个做成可变的。等弄完手里的事情,就对 m5 大修。

Avatar_small
rca 说:
2013年3月12日 15:50

@冷水: 暂时在 m5 中将输出的 m4 代码中的引号修改为 @???[[...]]???@,这样估计一般不会跟代码中的引号出现冲突了。


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter