数据即代码:元驱动编程
(感谢 @文艺复兴记(todd) 投递此文)
几个小伙伴在考虑下面这个各个语言都会遇到的问题:
问题:设计一个命令行参数解析API
一个好的命令行参数解析库一般涉及到这几个常见的方面:
1) 支持方便地生成帮助信息
2) 支持子命令,比如:git包含了push, pull, commit等多种子命令
3) 支持单字符选项、多字符选项、标志选项、参数选项等多种选项和位置参数
4) 支持选项默认值,比如:–port选项若未指定认为5037
5) 支持使用模式,比如:tar命令的-c和-x是互斥选项,属于不同的使用模式
经过一番考察,小伙伴们发现了这个几个有代表性的API设计:
1. getopt():
getopt()是libc的标准函数,很多语言中都能找到它的移植版本。
//C while ((c = getopt(argc, argv, "ac:d:")) != -1) { int this_option_optind = optind ? optind : 1; switch (c) { case 'a': printf ("option a"); aopt = 1; break; case 'c': printf ("option c with value '%s'", optarg); copt = optarg; break; case 'd': printf ("option d with value '%s'", optarg); dopt = optarg; break; case '?': break; default: printf ("?? getopt returned character code 0%o ??", c); } }
getopt()的核心是一个类似printf的格式字符串的命令行参数描述串,如上面的”ac:d:”定义了”a”, “c”,”d”3个命令行参数,其中,a是一个标志符不需要参数,”c”和”d”需要跟参数。getopt()功能非常弱,只支持单个字符的标志选项和参数选项。如果按上面的5点来比对,基本上只能说是勉强支持第3点,其他几项只能靠程序自己来实现了,所以,想直接基于getopt()实现一个像git这样复杂的命令行参数是不可能的,只有自己来做很多的解析工作。小伙伴们看过getopt()之后一致的评价是:图样图森破。
2. Google gflags
接着,小伙伴们又发现了gflags这个Google出品C++命令行参数解析库。
//C++ DEFINE_bool(memory_pool, false, "If use memory pool"); DEFINE_bool(daemon, true, "If started as daemon"); DEFINE_string(module_id, "", "Server module id"); DEFINE_int32(http_port, 80, "HTTP listen port"); DEFINE_int32(https_port, 443, "HTTPS listen port"); int main(int argc, char** argv) { ::google::ParseCommandLineFlags(&argc, &argv, true); printf("Server module id: %s", FLAGS_module_id.c_str()); if (FLAGS_daemon) { printf("Run as daemon: %d", FLAGS_daemon); } if (FLAGS_memory_pool) { printf("Use memory pool: %d", FLAGS_daemon); } Server server; return 0; }
小伙伴们看了后不由得感叹“真心好用啊”!的确,gflags简单地通过几个宏就定义了命令行选项,基本上很好的支持了上面提到的1,3,4这几项,比起getopt()来强多了。对于类似cp这样的小命令,gflags应该是够用了,但要达到git这种级别就显得有些单薄了。
3. Ruby Commander
接下来小伙伴们又发现了Ruby Commander库:
//Ruby # :name is optional, otherwise uses the basename of this executable program :name, 'Foo Bar' program :version, '1.0.0' program :description, 'Stupid command that prints foo or bar.' command :bar do |c| c.syntax = 'foobar bar [options]' c.description = 'Display bar with optional prefix and suffix' c.option '--prefix STRING', String, 'Adds a prefix to bar' c.option '--suffix STRING', String, 'Adds a suffix to bar' c.action do |args, options| options.default :prefix => '(', :suffix => ')' say "#{options.prefix}bar#{options.suffix}" end end $ foobar bar # => (bar) $ foobar bar --suffix '}' --prefix '{' # => {bar}
Commander库利用Ruby酷炫的语法定义了一种描述命令行参数的内部DSL,看起来相当高端大气上档次。除了上面的第5项之外,其他几项都有很好的支持,可以说Commander库的设计基本达到了git这种级别命令行参数解析的要求。只是,要搞懂Ruby这么炫的语法和这个库的使用方法恐怕就不如getopt()和gflags容易了。有小伙伴当场表示想要学习Ruby,但是也有小伙伴表示再看看其他库再说。
4. Lisp cmdline库
接下来,小伙伴们发现了Lisp方言Racket的cmdline库。
//Lisp (parse-command-line "compile" (current-command-line-arguments) `((once-each [("-v" "--verbose") ,(lambda (flag) (verbose-mode #t)) ("Compile with verbose messages")] [("-p" "--profile") ,(lambda (flag) (profiling-on #t)) ("Compile with profiling")]) (once-any [("-o" "--optimize-1") ,(lambda (flag) (optimize-level 1)) ("Compile with optimization level 1")] [("--optimize-2") ,(lambda (flag) (optimize-level 2)) (("Compile with optimization level 2," "which implies all optimizations of level 1"))]) (multi [("-l" "--link-flags") ,(lambda (flag lf) (link-flags (cons lf (link-flags)))) ("Add a flag <lf> for the linker" "lf")])) (lambda (flag-accum file) file) '("filename"))
这是神马浮云啊?括号套括号,看起来很厉害的样子,但又不是很明白。看到这样的设计,有的小伙伴连评价都懒得评价了,但也有的小伙伴对Lisp越发崇拜,表示Lisp就是所谓的终极语言了,没有哪门语言能写出这么不明觉历的代码来!小伙伴们正准备打完收工,突然…
5. Node.js的LineParser库
发现了Node.js的LineParser库:
[javascript]
//JavaScript
var meta = {
program : ‘adb’,
name : ‘Android Debug Bridge’,
version : ‘1.0.3’,
subcommands : [ ‘connect’, ‘disconnect’, ‘install’ ],
options : {
flags : [
[ ‘h’, ‘help’, ‘print program usage’ ],
[ ‘r’, ‘reinstall’, ‘reinstall package’ ],
[ ‘l’, ‘localhost’, ‘localhost’ ]
],
parameters : [
[ null, ‘host’, ‘adb server hostname or IP address’, null ],
[ ‘p’, ‘port’, ‘adb server port’, 5037 ]
]
},
usages : [
[ ‘connect’, [‘host’, ‘[port]’], null, ‘connect to adb server’, adb_connect ],
[ ‘connect’, [ ‘l’ ], null, ‘connect to the local adb server’, adb_connect ],
[ ‘disconnect’, null, null, ‘disconnect from adb server’, adb_disconnect ],
[ ‘install’, [‘r’], [‘package’], ‘install package’, adb_install ],
[ null, [‘h’], null, ‘help’, adb_help ],
]
};
try {
var lineparser = require(‘lineparser’);
var parser = lineparser.init(meta);
// adb_install will be invoked
parser.parse([‘install’, ‘-r’, ‘/pkgs/bird.apk’]);
}
catch (e) {
console.error(e);
}
[/javascript]
天啊!?这是什么?我和小伙伴们彻底惊呆了!短短十几行代码就获得了上面5点的全面支持,重要的是小伙伴们居然一下子就看懂了,没有任何的遮遮掩掩和故弄玄虚。本来以为Ruby和Lisp很酷,小伙伴们都想马上去学Ruby和Lisp了,看到这个代码之后怎么感觉前面全是在装呢?有个小伙伴居然激动得哭着表示:我写代码多年,以为再也没有什么代码可以让我感动,没想到这段代码如此精妙,我不由得要赞叹了,实在是太漂亮了!
小伙伴们的故事讲完了,您看懂了吗?如果没有看懂的话,正题开始了:
在绝大多数语言中数据和代码可以说是泾渭分明,习惯C++、Java等主流语言的程序员很少去思考数据和代码之间的关系。与多数语言不同的是Lisp以“数据即代码,代码即数据”著称,Lisp用S表达式统一了数据和代码的形式而独树一帜。Lisp奇怪的S表达式和复杂的宏系统让许多人都感到Lisp很神秘,而多数Lisp教程要么强调函数式编程,要么鼓吹宏如何强大,反而掩盖了Lisp真正本质的东西,为此我曾写过一篇《Lisp的永恒之道》介绍Lisp思想。
设计思想和具体技术的区别在于前者往往可以在不同的环境中以不同的形式展现出来。比如,熟悉函数式编程的程序员在理解了纯函数的优点后即使是用C语言也会更倾向于写出无副作用的函数来,这就是函数式思想在命令式环境的应用。所以,理解Lisp思想一定要能在非Lisp环境应用,才算是融汇贯通。
如果真正理解了Lisp的本质,那所谓的“数据即代码,代码即数据”一点儿也不神秘,这不就是我们每天打交道的配置文件吗!?如果你还不是很理解的话,我们通过下面几个问题慢慢分析:
1) 配置的本质是什么?为什么要在程序中使用配置文件?
不知道你是否意识到了,我们每天都在使用的各种各样的配置本质上是一种元数据也是一种DSL,这和Lisp基于S表达式的“数据即代码,代码即数据”没有本质区别。在C++、Java等程序中引入配置文件的目的正是用DSL弥补通用语言表达能力和灵活性的不足。我知道不少人喜欢从计算的角度来看到程序和语言,似乎只有图灵完备的语言如C++、Java、Python等才叫程序设计语言,而类似CSS和HTML这样的东西根本不能叫做程序设计语言。其实,在我看来这种观点过于狭隘,程序的本质是语义的表达,而语义表达不一定要是计算。
2) 配置是数据还是代码?
很明显,Both!说配置是数据,因为它是声明式的描述,能方便地修改和传输;说配置是代码,因为它在表达逻辑,你的程序实际上就是配置的解释器。
3) 配置的格式是什么?
配置的格式是任意的,可以自己定义语法,只要配以相应的解释器就行。不过更简单通用的做法是基于XML、JSON、或S表达式等标准结构,在此之上进一步定义schema。甚至完全不必是文件,在我们的项目中配置经常是放到用关系数据库中的。另外,下面我们还会看到用语言的Literal数据作为配置。
4) 业务逻辑都可以放到配置中吗?
这个问题的答案显然是:Yes!我没有遇到过不可以放入配置的逻辑,只是问题在于这样做是否值得,能达到什么效果。对于需要灵活变化,重复出现,有复用价值的东西放入作为配置是明智的选择。这篇文章的主要目的就在于介绍把主要业务逻辑都放到配置中,再通过程序解释执行配置的设计方法,我称之为:元驱动编程(Meta Driven Programming)。
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
《数据即代码:元驱动编程》的相关评论
为什么不直接传个object进去?
比如这样
find -name “*.txt” | my-prog “{cmd:\”sort\”, comp:function(a,b){strcmp(a,b);}}”
看过一篇文章,说是plan9中也有这么一个轮子
指出一个小小的错误
讲解gflags的时候,里面有行代码:printf(“Use memory pool: %d”, FLAGS_daemon);
这里的FLAGS_daemon 应该是:FLAGS_memory_pool
作者在这里,对语言理解的很深。把元驱动编程讲的很到位。当然在我没看这篇文章之前,我也不知道,什么是元驱动编程。虽然我之前经常会用到。
但在实际应用中,我还是更愿意把 数据提出来让数据结构去管理他。可是每次,都很难分干净。让我很苦劳。
高端
高端!支持酷壳,也向酷壳学习,我的网站仿的越越来越像酷壳了(除了内容),嘿嘿!到我的小窝去逛逛!
这种配置用YAML表达挺好的。文中是这几个库的设计问题,跟编程语言无关啦。
Lisp cmdline库
这段中,“不明觉历”应为“不明觉厉”
Lisp的配置文件最欢快了,直接把源码里面那些“配置”抠出来写在一个单独的lisp文件里面就行了
最近在看《程序员修炼之道:从小工到专家》刚好也有讲到“使用元数据”与“领域语言”,再加深一点理解
git也是表驱动的:
git.c:
static void handle_internal_command(int argc, const char **argv)
{
const char *cmd = argv[0];
static struct cmd_struct commands[] = {
{ “add”, cmd_add, RUN_SETUP | NEED_WORK_TREE },
{ “annotate”, cmd_annotate, RUN_SETUP },
{ “apply”, cmd_apply, RUN_SETUP_GENTLY },
{ “archive”, cmd_archive },
……………………..
for (i = 0; i cmd, cmd))
continue;
exit(run_builtin(p, argc, argv));
}
………………….
@凉拌茶叶
茶叶兄也在这里啊……
有没有发现 在虚拟的层层之上,程序语言越来越像自然语言
今天才知道,小时候我自己瞎琢磨出来的的组织参数的方法,还有个专门的名字,长见识了,以后再也不用觉得这么写很无赖很流氓很“脏”了,原来这也是挺高大上的一条路子呢
看看python的docopt,要比这些库都好用。不过思路是一样的。
这文章就是一软文,吹捧的LineParser其实是这文章作者自己的项目。写软文可以,写成这样就有点过了。
说起来,命令行解析,node下有好几个非常不错的,比如在npm依赖排行榜上第四位的optimist(https://github.com/substack/node-optimist)和排名第六位的commander(https://github.com/visionmedia/commander.js),作者分别是神人substack和神人visionmedia。
我喵了一眼,这LineParser的api比起上面两个那真是弱爆了,作者还是不理解什么才是好API。
提到元编程或者数据驱动,除了作为配置的微语言或者DSL,至少还应该提到 代码生成技术 或者 generative programming吧。足够简单的代码和足够复杂的数据的界限确是十分模糊的。策略与机制分离,机制用代码实现,策略以数据表述,本质上是声明式编程在两个方向上的分化。有些时候希望模糊代码与数据的界限,有些时候却要特别分清楚。否则的话为什么不用 SExpression(SXML)代替 XML + XML family 语族呢?
真不知道你们怎么想的, ,为什么我完全没有注意到你们吐槽的词… 也就评论里面的’小伙伴’最多了..
在我刚学会编程的第一年,某一天,我也想到了这个意思:“你的程序实际上就是配置的解释器。”@shuiren
@xiaoqiang
额,感觉文章提到的数据,更可能指的是程序的配置数据,将程序的某些逻辑需要根据业务变化,或者其他原因做到灵活配置,甚至动态配置。但是程序本身处理的是业务数据,业务数据如何影响到程序的运行呢?这个感觉形容在神经网络更适合(神经网络整个运行系统,不是指神经网络具体某个实现代码)。神经网络不就是处理业务数据的同时升级自身处理逻辑吗?这个逻辑也是以数据存在,虽然可能不为人理解。而本文指的形式需要人involve进这个过程,根据业务需要改变配置数据。业务数据到配置数据环节需要人的参与。
我是一个外行。在这眼花缭乱编程语言的背后,应该存在一个终极的元驱动编程,作者的思索令我钦佩,可称得起大师级的人物,但还不足,因为元驱编程早已存在于大自然当中,我们所作的一切努力都只不过是在更进一步地接近它而已。我真诚的希望作者能够跨上更高一级的台阶。