前言
本文以命令列參數解析為例,來展示 OCaml 程式設計的模式和反模式。
取得命令列參數
命令列參數儲存在 Sys.argv 陣列。其中 Sys.argv.(0) 是程式名稱,Sys.argv.(1) 以後是傳入的參數。Sys.argv 陣列元素的資料型態是字串。
在 OCaml 裡寫 C
這裡用命令式程式設計的思維來寫 OCaml 程式。範例程式碼如下:
let _ =
if Array.length Sys.argv <= 1 then (
Printf.eprintf "No source\n";
Printf.eprintf "Usage: cli [option] <source>\n";
exit 1
) else if Array.length Sys.argv > 2 then (
Printf.eprintf "Too many argyments\n";
Printf.eprintf "Usage: cli [option] <source>\n";
exit 1
) else if Sys.argv.(1) = "-v" || Sys.argv.(1) = "--version" then
Printf.printf "0.1.0\n"
else if Sys.argv.(1) = "-h" || Sys.argv.(1) = "--help" then
Printf.printf "Usage: cli [option] <source>\n"
else if String.starts_with ~prefix:"-" Sys.argv.(1) then (
Printf.eprintf "Unknown command: %s\n" Sys.argv.(1);
Printf.eprintf "Usage: cli [option] <source>\n";
exit 1
) else
Printf.printf "[Shim] Handle file: %s\n" Sys.argv.(1)
這裡的程式碼雖然是 OCaml,但沒用到 OCaml 特性,充其量只是「在 OCaml 裡寫 C」。這樣寫是合法的,但不應該這樣寫。
在 OCaml 裡寫 JavaScript
這裡開始在程式碼中加入架構。範例程式碼如下:
let program = "cli" (* 1 *)
let version = "0.1.0" (* 2 *)
let arg_parse argv = (* 3 *)
let size = Array.length argv in (* 4 *)
if size <= 1 then (* 5 *)
"empty" (* 6 *)
else if size > 2 then (* 7 *)
"too_many" (* 8 *)
else (* 9 *)
let arg = argv.(1) in (* 10 *)
if arg = "-v" || arg = "--version" then (* 11 *)
"version" (* 12 *)
else if arg = "-h" || arg = "--help" then (* 13 *)
"help" (* 14 *)
else if String.starts_with ~prefix:"-" arg then (* 15 *)
"unknown:" ^ arg (* 16 *)
else (* 17 *)
"file:" ^ arg (* 18 *)
let _ = (* 19 *)
match arg_parse Sys.argv with (* 20 *)
| "empty" -> (* 21 *)
Printf.eprintf "No source\n"; (* 22 *)
Printf.eprintf "Usage: %s [option] <source>\n" program; (* 23 *)
exit 1 (* 24 *)
| "too_many" -> (* 25 *)
Printf.eprintf "Too many arguments\n"; (* 26 *)
Printf.eprintf "Usage: %s [option] <source>\n" program; (* 27 *)
exit 1 (* 28 *)
| "version" -> (* 29 *)
Printf.printf "%s\n" version (* 30 *)
| "help" -> (* 31 *)
Printf.printf "Usage: %s [option] <source>\n" program (* 32 *)
| s when String.starts_with ~prefix:"unknown:" s -> (* 33 *)
let unknown = String.sub s 8 (String.length s - 8) in (* 34 *)
Printf.eprintf "Unknown command: %s\n" unknown; (* 35 *)
Printf.eprintf "Usage: %s [option] <source>\n" program; (* 36 *)
exit 1 (* 37 *)
| s when String.starts_with ~prefix:"file:" s -> (* 38 *)
let file = String.sub s 5 (String.length s - 5) in (* 39 *)
Printf.printf "[Shim] Handle file: %s\n" file (* 40 *)
| _ -> (* 41 *)
Printf.eprintf "Unhandled case\n"; (* 42 *)
exit 1 (* 43 *)
arg_parse 函式解析命令列參數 (第 3 至 18 行),主程式接收命令列參數,並執行程式 (第 19 至 43 行)。
這裡用字串傳遞狀態,沒有充份利用 OCaml 的特性,等同於把 OCaml 降級成弱型態語言。傳遞字串在 JavaScript 文化是相當常見的,所以這是「在 OCaml 裡寫 JavaScript」。雖然這樣寫是合法的,也不建議這樣寫。
充份使用 OCaml 特性的範例
看完前兩個版本的範例,我們回頭來用 OCaml 特性寫解析命令列參數的程式。
設置常數
建立一些常數,取代魔術字串:
let program = "cli"
let version = "0.1.0"
解析命令列參數
用 variant 宣告解析命令列參數所回傳的狀態:
type command_t =
| CmdVersion
| CmdHelp
| CmdFile of string
由於用到了 variant,會受到 OCaml 編譯器的保護。傳遞狀態時,編譯器會檢查是否滿足所有狀態。
實作解析命令列參數的工具函式:
(* Result (command_t, string) *)
let arg_parse argv =
match Array.to_list argv with
| [_] -> Error "No source"
| [_; "-v"] | [_; "--version"] -> Ok CmdVersion
| [_; "-h"] | [_; "--help"] -> Ok CmdHelp
| [_; arg] when String.starts_with ~prefix:"-" arg ->
Error (Printf.sprintf "Unknown option: %s" arg)
| [_; file] -> Ok (CmdFile file)
| _ -> Error "Too many arguments"
這裡根據命令列的「形狀」判斷狀態,所以很直觀。
印出幫助訊息
宣告串流狀態:
type stream_t =
| Stdout
| Stderr
注意這裡並不是真的標準輸出和標準錯誤,只是用這兩種狀態來命名 variant。
實作印出幫助訊息的工具函式:
let help_info stream =
let template = Printf.sprintf "Usage: %s [option] <source>" program in
match stream with
| Stdout -> print_endline template
| Stderr -> prerr_endline template
根據程式的狀態,同樣的訊息可導向標準輸出或標準錯誤,符合 Unix 慣例。
執行主程式
實作主程式:
(* main *)
let _ =
let cmd = arg_parse Sys.argv in
match cmd with
| Ok CmdVersion ->
Printf.printf "%s\n" version
| Ok CmdHelp ->
help_info Stdout
| Ok (CmdFile file) ->
Printf.printf "[Shim] Handle file: %s\n" file
| Error err ->
prerr_endline err;
help_info Stderr;
exit 1
由於我們已經實作好相關的工具函式,主程式很好寫。注意這裡利用了 pattern matching,OCaml 會協助開發者逐一完成所有情境。