位元詩人 [Common Lisp] 程式設計教學:使用 SBCL 或 Clozure CL 建立開發環境

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

在本文中,我們會建立 Common Lisp 開發環境,為撰寫 Common Lisp 程式做準備。

由於 Common Lisp 本身是語言標準,沒有官方實作品,現存的 Common Lisp 實作品間都有細微的差異。最好在選定 Common Lisp 實作品後就固定使用同一種 Common Lisp 編譯器或直譯器,以避免反覆修改程式碼。

由於 SBCL (Steel Bank Common Lisp)Clozure CL 是最普遍的 Common Lisp 實作品,我們會以這兩種編譯器為例,展示建立 Common Lisp 開發環境的方式。

安裝 SBCL

對於 Common Lisp 初學者來說,使用 SBCL 是最安全的選項。因為 SBCL 穩定、使用者多。Common Lisp 程式設計師在撰寫函式庫時,也會優先測試 SBCL。

Windows

到 SCBL 的下載頁面,選擇適用於 Windows 的安裝程式。啟動安裝程式後,按照安裝程式的指示即可。

macOS

由於官網提供的版本較舊,建議使用 Homebrew 來安裝。參考以下指令:

$ brew install sbcl

GNU/Linux

在 Debian/Ubuntu/Linux Mint 中可用以下指令來安裝 SBCL:

$ sudo apt install sbcl

在 openSUSE 中可用以下指令來安裝 SBCL:

$ sudo zypper install sbcl

不一定每個 GNU/Linux 發行版都用預編好的 SBCL 套件。若讀者使用的 GNU/Linux 發行版沒有 SBCL,可到官方網站的下載頁面下載預編好的 SBCL。參考以下指令來安裝 SBCL:

$ tar xf sbcl-2.0.3-x86-64-linux-binary.tar.bz2
$ cd sbcl-2.0.3-x86-64-linux
$ mkdir -p $HOME/opt/sbcl
$ INSTALL_ROOT=$HOME/opt/sbcl ./install.sh

然後將 $HOME/opt/sbcl/bin 加入 PATH 變數即可。若讀者想要安裝到其他位置,請自行修改指令。

FreeBSD

在 FreeBSD/TrueOS 中可以用以下指令來安裝 (需用 root):

# pkg install sbcl

FreeBSD 的 SBCL 的版本有點滯後,較不建議使用。

安裝 Clozure CL

Clozure CL 原本是運行在 macOS 上的 Common Lisp 實作品,後來才演變成跨平台軟體。所以,Clozure CL 對撰寫 macOS 平台的 GUI 程式有特別的優勢。

到 CLozure CL 的官方下載頁面,根據自己使用的系統來下載相對應的 Clozure CL 即可。

將 Clozure CL 的壓縮檔解壓縮後,Clozure CL 會存在 ccl 目錄中。將 ccl 目錄移動到任意位置後,再將 ccl 所在的位置加入 PATH 變數即可。

Clozure CL 在 macOS Mojave 上的解決方案 (Workaround)

在 macOS 上使用 Clozure CL 時可能會碰到以下的 issue

$ ccl --load quicklisp.lisp 
sigreturn returned
? for help
[3835] Clozure CL kernel debugger: 

原本 Clozure CL 的團隊建議下載 Clozure CL 的編譯器後再重編 Clozure CL。但筆者實測時發現會無法順利編譯。根據錯誤訊息來看,是某段組合語言程式碼出了問題。但筆者不會組語,沒辦法自己修,只好暫時繞過這個問題。

這裡下載現成的 darwinx86.tar.gzsource code (tar.gz) 後即可使用。參考以下指令:

$ mkdir $HOME/opt
$ cd $HOME/opt
$ wget -c https://github.com/Clozure/ccl/releases/download/v1.12-dev.5/darwinx86.tar.gz
$ wget -c https://github.com/Clozure/ccl/archive/v1.12-dev.5.tar.gz
$ tar xf ccl-1.12-dev.5.tar.gz
$ cd ccl-1.12-dev.5
$ tar xf ../darwinx86.tar.gz

基本上,我們跳過重編 Clozure CL 這個動作,直接用預編好的 Clozure CL。

使用 Roswell 管理 Common Lisp 實作品

Roswell 是一套 Common Lisp 管理軟體。有時候我們會在 Common Lisp 的學習資料上看到有關 Roswell 的敘述,因為 Roswell 也可用來安裝 Common Lisp 實作品。

由於 Roswell 的使用方式較複雜,我們會另開專文來敘述。對於初學者來說,若不想耗費太多時間在學開發工具上,可先專注在 SBCL 即可。

選擇適合 Common Lisp 的編輯器

以下是免費的 Common Lisp 編輯器:

除此之外,Allegro CL 和 LispWork 附帶商用的整合式開發環境。

許多 Common Lisp 的文章會建議 Emacs + SLIME 的組合。但對 Emacs 不熟的程式設計者來說,這樣的組合會讓人誤以為 Common Lisp 很難學,因為 Emacs 本身就是不太好學的編輯器。

使用 Emacs 搭配 SLIME (或 Sly) 的好處是可以整合編輯器和 REPL 環境。雖然 REPL 環境對於撰寫 Common Lisp 程式碼不是必要的,如果想要完整且免費的 Common Lisp 開發環境,Emacs 仍然是好選擇。我們會在後文介紹 SLIME 的使用方式。

對於不熟 Emacs 的讀者,筆者建議使用 VSCode + Lisp 外掛。雖然這個組合沒有整合 REPL 環境,但對 Common Lisp 有基本的支援 (語法高亮、中括號配對),而且設置簡單。搭配筆者自訂的命令稿,不太需要 REPL 環境也能寫 Common Lisp 程式。

使用 SBCL 的 REPL 環境

在啟動 SBCL 時,若不輸入參數,會進入 REPL (Read-Eval-Print Loop) 模式:

$ sbcl
This is SBCL 2.0.2, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.

SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses.  See the CREDITS and COPYING files in the
distribution for more information.
*

在 REPL 環境中,可以輸入 Common Lisp 指令,會得到立即性的回饋:

* (+ 4 3)
7

REPL 環境主要是拿來試簡短的程式碼,實際上還是會把程式碼存在命令稿 (script) 中。

使用 quit 指令可以離開 REPL 環境:

* (quit)

使用 Clozure CL 的 REPL 環境

在啟動 Clozure CL 時,若不輸入參數,會進入 REPL (Read-Eval-Print Loop) 模式:

$ ccl
Clozure Common Lisp Version 1.12-dev (v1.12-dev.5) DarwinX8664

For more information about CCL, please see http://ccl.clozure.com.

CCL is free software.  It is distributed under the terms of the Apache
Licence, Version 2.0.
?

實際上 Clozure CL 的指令通常不是 ccl,而會隨系統而異。在 64 位元 Windows 上的 Clozure CL 指令是 wx86cl64.exe。在 64 位元 GNU/Linux 上的 Clozure CL 指令是 lx86cl64。但我們在本系列文章中仍會用 ccl 代表 Clozure CL 指令。

在 REPL 環境中,可以輸入 Common Lisp 指令,會得到立即性的回饋:

? (+ 4 3)
7

REPL 環境主要是拿來試簡短的程式碼,實際上還是會把程式碼存在命令稿 (script) 中。

使用 quit 指令可以離開 REPL 環境:

? (quit)

但在 Windows 上使用某些版本的 Clozure CL 時,下達 quit 指令會導到整個終端機凍住 (freeze) (參考此 issue)。替代的方式是改用以下指令:

? (#_exit 0)

這個指令實際上是呼叫命令提示字元的 exit 指令。新版的 Clozure CL 已修復這項 bug。請使用 Clozure CL 的讀者更新系統的 Clozure CL 版本。

執行第一個程式

傳統的 Lisp 學習資源會認定程式設計者在 REPL 環境中輸入指令,但這和當今的程式設計的慣例差異較大,故本節介紹不使用 REPL 環境的撰碼方式。本節介紹的方式類似於撰寫 Python 或 Ruby 命令稿時所用的撰碼流程。

在編輯器中建立 hello.lisp 文字檔案,加入以下內容:

(write-line "Hello World")

以 SBCL 執行此腳本的指令如下:

$ sbcl --script hello.lisp
Hello World

--script 參數是指用批次模次執行 Common Lisp 命令稿,不會進入 REPL 環境。

如果讀者想要有主程式,可以將上述程式改寫如下:

;; Custom-made main function.
(defun main ()
  (write-line "Hello World")
  (quit))

我們暫時不講解程式碼的內容,以熟悉開發工具為主。

以 SBCL 載入此命令稿:

$ sbcl --noinform --load hello.lisp --eval "(main)"

以 Clozure CL 載入此命令稿:

$ ccl --load hello.lisp --eval "(main)"

原本 Common Lisp 在載入命令稿後會進入 REPL 環境,但我們刻意在程式尾端加上 (quit) 指令,其效果為離開 REPL 環境。整體的效果就像是以批次模式執行一個命令稿。

(錯誤排除) Common Lisp 命令稿在命令列不會輸出文字

若讀者在執行前述 Hello World 程式時,發現命令列不會輸出文字,這是因為 Common Lisp 編譯器把文字存在緩衝區 (buffer) 裡。在輸出文字後,加上 (finish-ouput) 指令即可。

將上述 Hello World 程式改寫如下:

;; Custom-made main function.
(defun main ()
  (write-line "Hello World")
  (finish-output) ; Trick for Clozure CL.
  (quit))

將 Common Lisp 命令稿編譯成執行檔

除了直接載入 Common Lisp 命令稿,也可以先將 Common Lisp 命令稿編譯為執行檔後再執行程式。平常在練習 Common Lisp 時,不需要先編譯再執行程式。本範例程式只是用來展示如何使用 Common Lisp 編譯器來編譯 Common Lisp 命令稿。

使用 SBCL 編譯執行檔

以編輯器建立 hello.lisp 文字檔案,輸入以下內容:

;; SBCL code

;; Main function.
(defun main ()
  (write-line "Hello World")
  (quit))

;; Compilation mode.
(defun compile-program ()
  (let ((program "hello"))
       (when (equal (software-type) "Win32")
         (setq program "hello.exe"))
       (sb-ext:save-lisp-and-die program
                                 :toplevel #'main
                                 :executable t)))

我們利用 sb-ext:save-lisp-and-die 指令將 Common Lisp 程式碼編譯成執行檔,然後再執行該執行檔。

以腳本執行該程式的指令如下:

$ sbcl --noinform --load hello.lisp --eval "(main)"

由於 main 函式內部已經呼叫 quit 函式,我們不需要再呼叫一次。

編譯後執行該腳本的指令如下:

$ sbcl --noinform --load hello.lisp --eval "(compile-program)" --quit
$ ./hello
Hello World

由於 compile-program 函式內部沒有呼叫 quit 函式,我們得再呼叫一次,才會直接離開程式。

經筆者實測,即使把 SBCL 移除,該執行檔仍然是可用的,代表該執行檔獨立 (standalone) 於 SBCL 開發環境外,可以發佈 (deploy) 到異地。

使用 Clozure CL 編譯執行檔

以編輯器建立 hello.lisp 文字檔案,輸入以下內容:

;; Clozure CL code.

;; Main function.
(defun main ()
  (write-line "Hello World")
  (finish-output) ; Trick for Clozure CL.
  (quit))

;; Compilation mode.
(defun compile-program ()
  (let ((program "hello"))
       (when (equal (software-type) "Microsoft Windows")
         (setq program "hello.exe"))
       (ccl:save-application program
                             :toplevel-function #'main
                             :prepend-kernel t)))

我們利用 ccl:save-application 指令將 Common Lisp 程式碼編譯成執行檔,然後再執行該執行檔。

以腳本執行該程式的指令如下:

$ ccl --load hello.lisp --eval "(main)"

由於 main 函式內已經呼叫 quit 函式了,我們不需要再呼叫一次。

編譯後執行該腳本的指令如下:

$ ccl --load hello.lisp --eval "(compile-program)" --eval "(quit)"
$ ./hello
Hello World

經筆者實測,即使把整個 Clozure CL 資料夾給刪除,該執行檔仍然是可用的,代表該執行檔獨立 (standalone) 於 Clozure CL 開發環境外,可以發佈 (deploy) 到異地。

讀者會發現 Clozure CL 命令稿和 SBCL 命令稿的語法是相異的,這是 Common Lisp 實作品之間的歧異性。在實務上,會利用條件編譯的手法來封裝其差異性,詳見後文。

(選擇性) 執行 SBCL 編譯器的命令稿

雖然 SBCL 支援統一碼 (unicode),但 SBCL 在預設設置下碰到中日韓等多國文字時,可能無法正確地顯示。為了處理這個議題,得加上額外的指令 (參考這裡這裡)。

本節所提供的命令稿將這些額外的指令包起來,省下重覆輸入指令的工夫。經筆者實測,如果 Common Lisp 程式碼中有中日韓等多國文字,本節的命令稿仍然是有效的。

適用於 Unix 的 Shell 命令稿

由於當代的 Unix 很多都已經使用 UTF-8 了,本小節的命令稿可能是不必要的。如果讀者有碰到多國文字的問題,再參考本小節的命令稿即可。

雖然很多 Unix 上會附 Bash,我們仍然刻意使用 POSIX shell 來寫命令稿。因為後者有較好的可攜性。

sbclrun 命令稿用來執行 Common Lisp 命令稿。其內容如下:

#!/bin/sh

# Check whether SBCL is available.
if ! command -v sbcl --version 2>/dev/null 1>&2;
then
  echo "No SBCL on the system" >&2;
  exit 1;
fi

script=$1;

if [ -f "$script" ];
then
  # Consume the first argument.
  shift;

  sbcl --noinform \
  --eval "(setf sb-impl::*default-external-format* :UTF-8)" \
  --eval "(load (concatenate 'string (sb-ext:posix-getenv \"HOME\") \"/\" \".sbclrc\"))" \
  --script "$script" "$@";
else
  sbcl "$@";
fi

這個命令稿會判斷第一個參數是否為檔案。當該參數是檔案時,以批次模式執行,並預寫好所需的參數。反之,則以一般模式執行。

適用於 Windows 的 Batch 命令稿

Windows 的腳本語言中,Batch 和舊系統的相容性最好,所以我們刻意使用 Batch 來寫命令稿。

sbclrun.bat 用來執行 Common Lisp 命令稿。其內容如下:

@echo off

rem Check whether SBCL is available.
sbcl --version >nul 2>&1 || (
    echo No SBCL on the system >&2
    exit /B 1
)

rem Get the path of a Lisp script from first argument.
set script=%1

rem Check whether the Lisp script is valid.
rem Run in batch mode if %script% is a file.
if exist %script% goto batch_mode

rem Fallback to interactive mode.
goto interactive_mode

:batch_mode
rem %script% is the first argument. Hence, there is no need to shift first argument.
sbcl --noinform ^
  --eval "(setf sb-impl::*default-external-format* :UTF-8)" ^
  --eval "(load (concatenate 'string (sb-ext:posix-getenv \"USERPROFILE\") \"\\\\\" \".sbclrc\"))" ^
  --script %*

rem Exit the program with inherited return value.
exit /B %ERRORLEVEL%

:interactive_mode
sbcl %*

如同前一小節的命令稿,這個命令稿會判斷第一個參數是否為檔案,並自動切換批次模式或一般模式。

(選擇性) 執行 Clozure CL 編譯器的命令稿

由於 Clozure CL 原本的指令輸入起來比較繁瑣,所以筆者寫了幾個命令稿來簡化輸入指令的動作。這些命令稿不是 Clozure CL 的一部分,而是筆者自行加入的小程式。如果讀者覺得這些命令稿沒有必要性,可略過本節無妨。

由於 Unix 和 Windows 命令列環境的腳本語言相異,我們分兩個情境來寫命令稿。

適用於 Unix 的 Shell 命令稿

雖然很多 Unix 上會附 Bash,我們仍然刻意使用 POSIX shell 來寫命令稿。因為後者有較好的可攜性。

ccl 命令稿用來執行 Common Lisp 命令稿。其內容如下:

#!/bin/sh

# Function to get the real command
#  of Clozure CL on a Unix host.
ccl () {
  if [ "Linux" = $(uname) ];
  then
    if [ "x86_64" = $(uname -m) ];
    then
      echo "lx86cl64";
    else
      echo "lx86cl";
    fi
  elif [ "Darwin" = $(uname) ];
  then
    if [ "x86_64" = $(uname -m) ];
    then
      echo "dx86cl64";
    else
      echo "dx86cl";
    fi
  elif [ "FreeBSD" = $(uname) ];  # Untested
  then
    if [ "x86_64" = $(uname -m) ];
    then
      echo "fx86cl64";
    else
      echo "fx86cl";
    fi
  elif [ "SunOS" = $(uname) ];  # Untested
  then
    if [ "x86_64" = $(uname -m) ];
    then
      echo "sx86cl64";
    else
      echo "sx86cl";
    fi
  else
    echo "Unsupported platform" >&2;
    exit 1;
  fi
}

# Check whether Clozure CL is available.
if ! command -v "$(ccl)" --version 2>/dev/null 1>&2;
then
  echo "No Clozure CL on the system" >&2;
  exit 1;
fi

script="$1";

if [ -f "$script" ];
then
  # Consume the first argument.
  shift;

  "$(ccl)" \
  --eval "(load (concatenate 'string (ccl:getenv \"HOME\") \"/\" \".ccl-init.lisp\"))" \
  --load "$script" -- "$@";
else
  "$(ccl)" "$@";
fi

我們假定 ccl 命令稿位於 Clozure CL 主程式的根目錄。由於 Clozure CL 在不同系統上有不同執行檔名稱,我們用命令稱封裝實際的執行檔名稱,簡化執行 Clozure CL 的動作。

同樣地,這個命令稿會自動判斷第一個參數是否為檔案,並切換批次模式或一般模式。

適用於 Windows 的 Batch 命令稿

Windows 的腳本語言中,Batch 和舊系統的相容性最好,所以我們刻意使用 Batch 來寫命令稿。

ccl.bat 用來執行 Common Lisp 命令稿。其內容如下:

@echo off

rem Set CCL according to hardware archtecture.
if "AMD64" == "%PROCESSOR_ARCHITECTURE%" (
    set ccl=wx86cl64.exe
) else (
    set ccl=wx86cl.exe
)

rem Check whether Clozure CL is available.
%ccl% --version 2>nul 1>&2 || (
    echo No Clozure CL on the system >&2
    exit /B 1
)

rem Get the root path of current batch script.
set rootdir=%~dp0

rem Get the path of a Lisp script from first argument.
set script=%1

rem Check whether the Lisp script is valid.
rem Run in batch mode if %script% is a file.
if exist %script% goto batch_mode

rem Fallback to interactive mode.
goto interactive_mode

:batch_mode
rem Consume one argument, which is %script%
shift

set args=
:collect_args
set arg=%1
if not "x%arg%" == "x" (
    set args=%args% %arg%

    rem Consume one more argument.
    shift

    goto collect_args
)

%ccl% ^
    --eval "(load (concatenate 'string (ccl:getenv \"USERPROFILE\") \"\\\\\" \"ccl-init.lisp\"))" ^
    --load %script% -- %args%

rem Exit the program with inherited return value.
exit /B %ERRORLEVEL%

:interactive_mode
%ccl% %*

使用時要將 ccl.bat 放在 Clozure CL 編譯器的根目錄。由於不同架構的 Windows 的 Clozure CL 執行檔相異,我們用命令稿封裝呼叫 Clozure CL 執行檔的過程,以簡化指令。

如同先前的命令稿,此命令稿會偵測第一個參數是否為檔案,並切換批次模式或一般模式。

查詢 Common Lisp 的 API

由於 Common Lisp 算小眾語言,網路上的學習資源相對貧乏,還是要學著看官方 API 文件來自學。HyperSpec 網站是由 LispWorks 所提供的 Common Lisp API 文件。在 Common Lisp 社群的地位相當於半官方文件。

如果讀者想看 Common Lisp 的書籍,可以看 Common LISP. The Language. Second Edition 。這本書相當於 Common Lisp 的聖經,也可用來查詢 API。但這本書已經是舊書了,實體書不太好找。讀者可自行在網路上找找看這本書的電子版本。

Common Lisp 的標準在西元 1994 年後就沒再修改了。由於 Common Lisp 是相對小眾的語言,資訊界應該不會有興趣再去修改這個語言的標準。因此,90 年代的 Lisp 學習資料仍然有其參考價值,並不會因年代較久就過時。

附記

本節所介紹的 wrapper 收錄在 cl-portable 專案。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。