數據即代碼:元驅動編程

幾個小夥伴在考慮下面這個各個語言都會遇到的問題:html

問題:設計一個命令行參數解析APInode

一個好的命令行參數解析庫通常涉及到這幾個常見的方面:git

1) 支持方便地生成幫助信息程序員

2) 支持子命令,好比:git包含了push, pull, commit等多種子命令github

3) 支持單字符選項、多字符選項、標誌選項、參數選項等多種選項和位置參數shell

4) 支持選項默認值,好比:–port選項若未指定認爲5037數據庫

5) 支持使用模式,好比:tar命令的-c和-x是互斥選項,屬於不一樣的使用模式編程

通過一番考察,小夥伴們發現了這個幾個有表明性的API設計:ruby

1. getopt():函數式編程

getopt()是libc的標準函數,不少語言中都能找到它的移植版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//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++命令行參數解析庫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//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庫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//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庫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//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庫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//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)

相關文章
相關標籤/搜索