使用 SBCL 或 Clozure CL 建立開發環境

    前言

    在本文中,我們會建立 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 上使用 quit 指令時會導到整個終端機凍住 (freeze) (參考此 issue)。替代的方式是改用以下指令:

    ? (#_exit 0)
    

    這個指令實際上是呼叫命令提示字元的 exit 指令。或許新版的 Clozure CL 會修復這項 bug。

    執行第一個程式

    傳統的 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
    # The script assumes that SBCL is in system path.
    
    # Check whether SBCL is available.
    if ! command -v sbcl 2>/dev/null 1>&2;
    then
      echo "Please install SBCL" >&2;
      exit 1;
    fi
    
    # Check whether the Lisp script is available.
    script=$1;
    
    if [ -z "$script" ] || [ ! -f "$script" ];
    then
      echo "Not a valid Lisp script" >&2;
      exit 1;
    fi
    
    # Consume first argument.
    shift;
    
    # Run SBCL with UTF-8 external format in batch mode.
    sbcl --noinform --eval "(setf sb-impl::*default-external-format* :UTF-8)" --script "$script" "$@";
    

    使用方式如下:

    $ sbclrun path/to/source.lisp
    

    但上述命令稿無法用來執行 SBCL 的 REPL 環境,故我們另外寫了 sbclrepl 。該 shell 命令稿會原封不動地把命令列參數帶結 SBCL 編譯器。其內容如下:

    #!/bin/sh
    # The script assumes that SBCL is in system path.
    
    # Check whether SBCL is available.
    if ! command -v sbcl 2>/dev/null 1>&2;
    then
      echo "Please install SBCL" >&2;
      exit 1;
    fi
    
    # Run SBCL with UTF-8 external format.
    sbcl --eval "(setf sb-impl::*default-external-format* :UTF-8)" "$@";
    

    使用時和直接呼叫 SBCL 編譯器無異:

    $ sbclrepl
    

    適用於 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" && exit 1)
    
    rem Get the path of a Lisp script from first argument.
    set script=%1
    
    rem Check whether the Lisp script is valid.
    if not exist %script% do echo "Not a valid Lisp script" && exit /b 1
    
    rem Consume first argument.
    shift
    
    rem Run SBCL in batch mode, setting the external format to UTF-8.
    sbcl --noinform --eval "(setf sb-impl::*default-external-format* :UTF-8)" --script %*
    

    使用方式如下:

    > sbclrun.bat path\to\source.lisp
    

    sbclrun.bat 無法執行 REPL 環境。故我們另外寫了個 sbclrepl.bat 命令稿來執行 REPL 環境。其內容如下:

    @echo off
    
    rem Check whether SBCL is available.
    sbcl --version >nul 2>&1 || (echo "No SBCL on the system" && exit 1)
    
    rem Run SBCL, setting the external format to UTF-8
    sbcl --eval "(setf sb-impl::*default-external-format* :UTF-8)" %*
    

    呼叫該命令稿即可啟動 SBCL 的 REPL 環境:

    > sbclrepl.bat
    

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

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

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

    適用於 Unix 的 Shell 命令稿

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

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

    #!/bin/sh
    # Place the script to the root path of Clozure CL.
    
    if [ "Linux" = $(uname) ];
    then
      if [ "x86_64" = $(uname -m) ];
      then
        ccl="lx86cl64";
      else
        ccl="lx86cl";
      fi
    elif [ "Darwin" = $(uname) ];
    then
      if [ "x86_64" = $(uname -m) ];
      then
        ccl="dx86cl64";
      else
        ccl="dx86cl";
      fi
    elif [ "FreeBSD" = $(uname) ];  # Untested
    then
      if [ "x86_64" = $(uname -m) ];
      then
        ccl="fx86cl64";
      else
        ccl="fx86cl";
      fi
    elif [ "SunOS" = $(uname) ];  # Untested
    then
      if [ "x86_64" = $(uname -m) ];
      then
        ccl="sx86cl64";
      else
        ccl="sx86cl";
      fi
    else
      echo "Unsupported platform" >&2;
      exit 1;
    fi
    
    root=$(dirname "$0");
    
    script=$1;
    
    if [ -z "$script" ] || [ ! -f "$script" ];
    then
      echo "Not a valid Lisp script" >&2;
      exit 1;
    fi
    
    # Consume first argument.
    shift;
    
    "$root/$ccl" -l "$script" "$@";
    

    我們假定 ccl 位於 Clozure CL 的根目錄。使用方式如下:

    $ cclrun path/to/source.lisp
    

    若我們在 source.lisp 的尾端加入 quit 指令,執行完該 Common Lisp 命令稿後就會結束 Clozure CL 編譯器。這樣的撰碼流程和主流直譯語言大抵上雷同。

    但上述命令稿無法用來執行 Clozure CL 的 REPL 環境,故我們另外寫了 cclrepl 。該 shell 命令稿會原封不動地把命令列參數帶結 Clozure CL 編譯器。其內容如下:

    #!/bin/sh
    # Place the script to the root path of Clozure CL.
    
    if [ "Linux" = $(uname) ];
    then
      if [ "x86_64" = $(uname -m) ];
      then
        ccl="lx86cl64";
      else
        ccl="lx86cl";
      fi
    elif [ "Darwin" = $(uname) ];
    then
      if [ "x86_64" = $(uname -m) ];
      then
        ccl="dx86cl64";
      else
        ccl="dx86cl";
      fi
    elif [ "FreeBSD" = $(uname) ];  # Untested
    then
      if [ "x86_64" = $(uname -m) ];
      then
        ccl="fx86cl64";
      else
        ccl="fx86cl";
      fi
    elif [ "SunOS" = $(uname) ];  # Untested
    then
      if [ "x86_64" = $(uname -m) ];
      then
        ccl="sx86cl64";
      else
        ccl="sx86cl";
      fi
    else
      echo "Unsupported platform" >&2;
      exit 1;
    fi
    
    root=$(dirname "$0");
    
    "$root/$ccl" "$@";
    

    同樣地, cclrepl 位於 Clozure CL 的根目錄。使用時和直接呼叫 Clozure CL 編譯器無異:

    $ cclrepl
    

    適用於 Windows 的 Batch 命令稿

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

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

    @echo off
    rem Place the script to the root path of Clozure CL.
    
    rem Set CCL according to hardware archtecture.
    if "AMD64" == "%PROCESSOR_ARCHITECTURE%" (set ccl=wx86cl64.exe) else (set ccl=wx86cl.exe)
    
    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.
    if not exist %script% do echo "Not a valid Lisp script" && exit /b 1
    
    rem Consume first argument.
    shift
    
    rem Run Clozure CL compiler.
    %rootdir%\%ccl% -l %*
    

    使用時要將 cclrun.bat 放在 Clozure CL 編譯器的根目錄。使用方式如下:

    > cclrun.bat path\to\source.lisp
    

    cclrun.bat 無法執行 REPL 環境。故我們另外寫了個 cclrepl.bat 命令稿來執行 REPL 環境。其內容如下:

    @echo off
    rem Place the script to the root path of Clozure CL.
    
    rem Set CCL according to hardware archtecture.
    if "AMD64" == "%PROCESSOR_ARCHITECTURE%" (set ccl=wx86cl64.exe) else (set ccl=wx86cl.exe)
    
    rem Get the root path of current batch script.
    set rootdir=%~dp0
    
    rem Run Clozure CL compiler.
    %rootdir%\%ccl% %*
    

    同樣地,使用時要將 cclrepl.bat 放在 Clozure CL 編譯器的根目錄。呼叫該命令稿即可啟動 Clozure CL 的 REPL 環境:

    > cclrepl.bat
    

    查詢 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 年後就沒再修改了,所以 90 年代的學習資料仍然有其參考價值,並不會因年代較久就過時。

    附記

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

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Yahoo
    【追蹤本站】
    Facebook Facebook Twitter Plurk