數據即代碼,我和我的小伙伴們都驚呆了
幾個小伙伴在考慮下面這個各個語言都會遇到的問題:
問題:設計一個命令行參數解析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)。