数据即代码:元驱动编程

数据即代码:元驱动编程

(感谢 @文艺复兴记(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
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)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——
好烂啊有点差凑合看看还不错很精彩 (53 人打了分,平均分: 4.25 )
Loading...

数据即代码:元驱动编程》的相关评论

  1. 技术文章能不能不要用“图样图森破”、“不明觉厉”这些词,我这种老古董看不明啊

  2. 数据结构很重要,数据的结构让数据具有可描述性,减少了代码量。我们不在需要在代码中将数据变换成特定的格式,这部分代码就省去了。除此之外,数据结构也决定的代码的走向,这又增加了代码本身的灵活性。高,实在是高。

  3. 例子是库吧,要说到数据及代码和语言级别的支持,最后一个例子用scala的match不是可以做得更精妙吗

  4. JSP的那个倒是看懂了。 对于解析字符串而言,脚本语言的优势太大了。 对于博主大大,如果将程序开发过程编程 脚本的解析过程, 这中开发理念就需要取决于 脚本解析器的强大程度。 但是针对您提到的Lisp的本质问题,提个问题:“这种开发方式是否可以简单的理解为 ‘游戏程序读取地图数据,产生不同的游戏场景’ 的开发方式呢?“

  5. 任何一个业务程序只要足够复杂,到最后都会发展成一个解释器(interpret)。对这点深有体会。文章的例子还是比较清楚的,个人感觉 json / lua 比 S 表达式易读。

  6. boost的命令行解析库呢?
    语言设计就是库设计,一门语言是否好用,就是库是否好用,仅此而已

  7. JS那个例子的特定程度跟其他几个完全不一样嘛。
    话说回来,这种配置式的写法,属于”写完之前不知道它到底该是什么样子”,写好了读起来可能比较舒服,真要新写一个倒是有一些学习成本。如果还没有详尽的文档,那就是一个灾难。

  8. 什么“元驱动编程”,无非就是那句话:配置文件复杂到一定程度就变成了一门语言。这不就是脚本语言的由来么。不是什么新概念,干嘛要搞得神神叨叨…

  9. 照此说脚本语言里的脚本基本上都只是配置而已,后面都有个解释器在解释执行。

  10. 我一直奇怪,*nix的天才们怎么没采用ini作为命令行参数的格式。
    现在我做系统,都是delphi的界面程序 调用 c的命令行工具,调用格式就是ini:
    ccmd.exe act=xxx arga=xxx argb=xxx …
    非常灵活方便,扩展性、兼容性非常好。
    而且命令行的分析代码更简单!

    ini应该是对效率(二进制、tvl)和可读性(xml、json)之间较合理的平衡了

  11. 我理解的程序一定是可执行的,即“流程”或“步骤”,而与之相对的则是用来描述的,比如配置或者数据,描述性的东西等价于自然语言。
    程序是无法直接表达的,它是语言所内涵的语义,用任何编程语言所表达出来的程序代码已经不是“程序”,而是“易于让计算机和读者理解的描述”,真正的“程序”是这些代码的含义。

  12. Zeicold :@haitao .ini 文件是一个无固定标准格式的设置档。另外好像 *nix 里没有扩展名的概念

    ini只是一个名字,就像xml/json,它其实就是key=value的描述协议。
    扩展名?ccmd.exe?这只是例子,*nix下,自然是 ./cmdapp了

  13. 看到吐槽的居多, 很不解, 或许大神比较多。。。

    文章开头对命令行参数parse程序的5个目标总结得非常到位。佩服! 不说别的, 如果能对解决的问题能做出如此准确的分解和归纳 我觉得阅读此文已经值回”票价“了, 何况我还没有买票。

    从printf的表达式到ruby里的dsl,某种意义上本质都是一样的。 如果有软件开发解决一些比较复杂问题的经验, 我觉得应该对本文的观点有一些共鸣。作者只是在分享他的感悟,而且内容真的是有货。我很感谢他的分享。

  14. 是不怎么样哦。
    库的问题和语言没什么关系。
    getopt是posix定的标准,和C没什么大关系,倒是va_list等这东西和C语言有关系。
    具体的实现可以参考glibc/posix/getopt.c

  15. C语言的getopt完全可以自己写。。就是个分析int argc,char **argv的东西。。
    当然了能分析这么多的语言还是很厉害。我就只会C。

  16. 数据驱动模式是非常强大,能做到的话可以把上千行的c代码简化成几十上百行的代码。而且清晰度极佳,算是一种化蛮力与脑力的差别体现了。很配服能把复杂的逻辑代码简化为数据结构的人

发表评论

电子邮件地址不会被公开。 必填项已用*标注