Java 的可變參數在 ABCL 中以陣列傳遞。本文示範如何建立 #L dispatch reader macro,簡化 Java 物件陣列的寫法,並逐步擴充為可指定陣列元素類型的語法;自訂 reader 雖然方便,也會帶來工具支援和載入順序等維護成本。
Java和JavaFX有不少使用可變參數的方法,例如:
VBox(Node... children)
Alert(Alert.AlertType alertType, String contentText, ButtonType... buttons)
Application 的靜態方法:static void launch(Class<? extends Application> appClass, String... args)
main(String...args)
在Java與ABCL互動時,最後一個可變參數會被視為物件陣列。以上方法的可變參數形式,可視為接受下列陣列形式:
VBox(Node[] children)
Alert(Alert.AlertType alertType, String contentText, ButtonType[] buttons)
Application 的靜態方法:static void launch(Class<? extends Application> appClass, String[] args)
main(String[] args)
#L 讀取巨集在ABCL中,要建立Java物件陣列可以使用#'jarray-from-list或#'jnew-array-from-list。由於這類寫法常會出現,而我希望有更簡潔的記法,便仿照#"建立另一個讀取巨集#L:
(set-dispatch-macro-character #\# #\L #'|#L-reader|)
讀取邏輯由#'|#L-reader|負責,先從最簡單的版本開始:
(defun |#L-reader| (stream subchar arg)
(declare (ignore subchar arg))
(let ((next-form (read stream nil nil nil)))
`(jarray-from-list ,next-form)))
在這個版本中,reader只讀取下一個form,並將它和jarray-from-list組成新的form。原本的寫法:
(jarray-from-list (list label-java-version label-javafx-version))
可以簡化成:
#L(list label-java-version label-javafx-version)
這個例子也可以用反引號建立list,省略明寫
list函式:(jarray-from-list `(,label-java-version ,label-javafx-version))當然也可以換成
#L`(,label-java-version ,label-javafx-version)不過同時使用兩個讀取巨集,可能需要特別小心。
#'jarray-from-list適用於元素屬於同一類別、ABCL可以自動推斷陣列類型的情況。若元素屬於不同類別,就需要指定共同的陣列元素類型,改用#'jnew-array-from-list,例如:
(new +VBox+ 50.5 (jnew-array-from-list "javafx.scene.Node"
(list label-java-version
label-javafx-version
btn-hello)))
這個例子中,label-java-version和label-javafx-version都是Label,而btn-hello是Button,因此要指定它們的共同父類別javafx.scene.Node。接著修改#'|#L-reader|,讓它依據參數執行不同邏輯:
(defun |#L-reader| (stream subchar arg)
(declare (ignore subchar))
(ecase arg
(1 (let ((type-form (read stream t nil t)))
`(jnew-array-from-list ,type-form nil)))
(2 (let ((type-form (read stream t nil t))
(value-form (read stream t nil t)))
`(jnew-array-from-list ,type-form ,value-form)))
((nil) (let ((next-form (read stream t nil t)))
`(jarray-from-list ,next-form)))))
別忘了重新設定讀取巨集:
(set-dispatch-macro-character #\# #\L #'|#L-reader|)
現在#2L會讀取兩個form:物件陣列的類型,以及陣列元素。因此可以改寫成:
(new +VBox+ 50.5 #2L "javafx.scene.Node"
(list label-java-version
label-javafx-version
btn-hello))
不帶數字參數時,#L的用法和最初版本相同。如果參數是1,則可以方便地建立不指定初始值的陣列,例如:
(jstatic "launch" +application+ +MY-APP+ (jnew-array "java.lang.String" 1))
可以簡化成:
(jstatic "launch" +application+ +MY-APP+ #1L"java.lang.String")
Alert也可以用更接近Java的方式建立:
// 不需要明確指定ButtonType。Alert有預設形式
Alert alert = new Alert(Alert.AlertType.INFORMATION, "Alert");
alert.show();
對應上面Java的LISP版本:
;; 仍要指定最後一個參數是 +ButtonType+ 物件陣列,
;; 但不必明確建立 OK 按鈕
(let ((alert (new +Alert+ +AlertType.INFORMATION+ "Alert" #1L+ButtonType+)))
(#"show" alert))
也可以再建立
#'make-alert,讓用法更貼近new Alert():(defun make-alert (alert-type message &rest button-types) (new +Alert+ alert-type message #2L+ButtonType+ button-types))有了
#'make-alert後,就可以這樣使用:(let ((alert (make-alert +AlertType.INFORMATION+ "Alert"))) (#"show" alert))要自訂按鈕也可以:
(let ((alert (make-alert +AlertType.INFORMATION+ "Alert" +ButtonType.OK+ +ButtonType.CANCEL+))) (#"show" alert))
不過,#2L+ButtonType+ (list +ButtonType.OK+ +ButtonType.CANCEL+)這種用法可能不受部分編輯器完整支援。可以再改寫#'|#L-reader|,讓它只讀取一個form,再依據參數以不同方式解讀內容:
(defun |#L-reader| (stream subchar arg)
(declare (ignore subchar))
(ecase arg
(1 (let ((next-form (read stream t nil t)))
`(jnew-array-from-list ,next-form nil)))
(2 (let ((next-form (read stream t nil t)))
`(jnew-array-from-list ,(car next-form) ,(cadr next-form))))
((nil) (let ((next-form (read stream t nil t)))
`(jarray-from-list ,next-form)))))
(set-dispatch-macro-character #\# #\L #'|#L-reader|)
參數為2時,改成以下寫法:#2L(+ButtonType+ (list +ButtonType.OK+ +ButtonType.CANCEL+))
這兩種寫法沒有絕對的優劣;選擇時更重要的是考量編輯器和其他 Lisp 工具對自訂語法的支援。若工具較難處理分開傳入型別和值的形式,將它們包在同一個 form 中可能更合適。
讀取巨集很方便,但也有普通函式沒有的代價:
因此,只有在這種語法能明顯簡化重複程式碼,而且專案能確保reader定義先載入、開發工具也能接受時,才值得採用;否則使用普通函式通常更容易維護。