数据即代码,我和我的小伙伴们都惊呆了
几个小伙伴在考虑下面这个各个语言都会遇到的问题:
问题:设计一个命令行参数解析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 flagfor the linker" "lf")])) (lambda (flag-accum file) file) '("filename"))
这是神马浮云啊?括号套括号,看起来很厉害的样子,但又不是很明白。看到这样的设计,有的小伙伴连评价都懒得评价了,但也有的小伙伴对Lisp越发崇拜,表示Lisp就是所谓的终极语言了,没有哪门语言能写出这么不明觉历的代码来!小伙伴们正准备打完收工,突然…
5. Node.js的LineParser库
发现了Node.js的LineParser库:
//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); }
天啊!?这是什么?我和小伙伴们彻底惊呆了!短短十几行代码就获得了上面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)。