iT邦幫忙

2026 iThome 鐵人賽

0
Software Development

玩轉ABCL Lisp!用奇怪偏門的方式學習JavaFX系列 第 34 篇

lesson 25(S4): 在 ABCL 中自訂讀取巨集:用 `#L` 簡化建立 Java 物件陣列的方式

  • 分享至 

  • xImage
  •  

TL;DR

Java 的可變參數在 ABCL 中以陣列傳遞。本文示範如何建立 #L dispatch reader macro,簡化 Java 物件陣列的寫法,並逐步擴充為可指定陣列元素類型的語法;自訂 reader 雖然方便,也會帶來工具支援和載入順序等維護成本。

Java 可變參數與 ABCL 陣列

Java和JavaFX有不少使用可變參數的方法,例如:

  • VBox 建構子:VBox(Node... children)
  • Alert 建構子:Alert(Alert.AlertType alertType, String contentText, ButtonType... buttons)
  • JavaFX Application 的靜態方法:static void launch(Class<? extends Application> appClass, String... args)
  • Java程式的主類別進入點: main(String...args)

在Java與ABCL互動時,最後一個可變參數會被視為物件陣列。以上方法的可變參數形式,可視為接受下列陣列形式:

  • VBox 建構子:VBox(Node[] children)
  • Alert 建構子:Alert(Alert.AlertType alertType, String contentText, ButtonType[] buttons)
  • JavaFX Application 的靜態方法:static void launch(Class<? extends Application> appClass, String[] args)
  • Java程式的主類別進入點: 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))

改用單一 form 的語法

不過,#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 中可能更合適。

讀取巨集的取捨

讀取巨集很方便,但也有普通函式沒有的代價:

  • 程式碼在讀取期就可能失敗,錯誤位置有時不容易理解,不易除錯。
  • 編輯器、formatter或其他Lisp工具不一定完整支援自訂reader syntax。
  • 讀取巨集定義必須先載入,否則同一份程式碼可能在不同環境無法讀取。
  • 如果把太多語意塞進reader,程式會變成只有作者自己知道的方言。

因此,只有在這種語法能明顯簡化重複程式碼,而且專案能確保reader定義先載入、開發工具也能接受時,才值得採用;否則使用普通函式通常更容易維護。


上一篇
lesson 24(S4): 怎麼把ABCL System打包成JAR檔案?
系列文
玩轉ABCL Lisp!用奇怪偏門的方式學習JavaFX 共 34 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言