Y分鐘學Common Lisp

ANSI Common Lisp 是一個普遍通用於各個工業領域的、支持多種範式的編程語言。 這門語言也常常被引用做「可編程的編程語言」(能夠寫代碼的代碼)。git

免費的經典的入門書籍《實用 Common Lisp 編程》github

另外還有一本熱門的近期出版的 Land of Lisp.編程

語法

通常形式

Lisp有兩個基本的語法單元:原子(atom),以及S-表達式。通常的,一組S-表達式被稱爲「組合式」。數組

10  ; 一個原子; 它對自身進行求值

:THING ;一樣是一個原子;它被求值爲一個符號 :thing

t  ;仍是一個原子,表明邏輯真值。

(+ 1 2 3 4) ; 一個S-表達式。

'(4 :foo  t)  ;一樣是一個S-表達式。

註釋

一個分號開頭的註釋表示僅用於此行(單行);兩個分號開頭的則表示一個所謂標準註釋;三個分號開頭的意味着段落註釋;而四個分號開頭的註釋用於文件頭註釋(譯者注:即對該文件的說明)。數據結構

#| 塊註釋
   能夠涵蓋多行,並且...
    #|
       他們能夠被嵌套!
    |#
|#

運行環境

有不少不一樣的Common Lisp的實現;而且大部分的實現是一致(可移植)的。對於入門學習來講,CLISP是個不錯的選擇。app

能夠經過QuickLisp.org的Quicklisp系統管理你的庫。less

一般,使用文本編輯器和「REPL」來開發Common Lisp;(譯者注:「REPL」指讀取-求值-打印循環)。「REPL」容許對程序進行交互式的運行、調試,就好像在系統「現場」操做。編程語言

基本數據類型以及運算符

符號編輯器

'foo ; => FOO  注意到這個符號被自動轉換成大寫了。

;; `intern`由一個給定的字符串而建立相應的符號

(intern "AAAA") ; => AAAA

(intern "aaa") ; => |aaa|

數字函數

9999999999999999999999 ; 整型數
#b111                  ; 二進制 => 7
#o111                  ; 八進制 => 73
#x111                  ; 十六進制 => 273
3.14159s0              ; 單精度
3.14159d0              ; 雙精度
1/2                    ; 分數
#C(1 2)                ; 複數

使用函數時,應當寫成這樣的形式:(f x y z ...)

其中,f是一個函數(名),x, y, z爲參數;若是你想建立一個「字面」意義上(即不求值)的列表, 只需使用單引號',從而避免接下來的表達式被求值。即,只「引用」這個數據(而不求值)。

'(+ 1 2) ; => (+ 1 2)

你一樣也能夠手動地調用一個函數(譯者注:即便用函數對象來調用函數):

(funcall #'+ 1 2 3) ; => 6

一些算術運算符

(+ 1 1)              ; => 2
(- 8 1)              ; => 7
(* 10 2)             ; => 20
(expt 2 3)           ; => 8
(mod 5 2)            ; => 1
(/ 35 5)             ; => 7
(/ 1 3)              ; => 1/3
(+ #C(1 2) #C(6 -4)) ; => #C(7 -2)

布爾運算

t                    ; 邏輯真(任何不是nil的值都被視爲真值)
nil                  ; 邏輯假,或者空列表
(not nil)            ; => t
(and 0 t)            ; => t
(or 0 nil)           ; => 0

字符

#\A                  ; => #\A
#\λ                  ; => #\GREEK_SMALL_LETTER_LAMDA(希臘字母Lambda的小寫)
#\u03BB              ; => #\GREEK_SMALL_LETTER_LAMDA(Unicode形式的小寫希臘字母Lambda)

字符串被視爲一個定長字符數組

"Hello, world!"
"Benjamin \"Bugsy\" Siegel"   ;反斜槓用做轉義字符

能夠拼接字符串

(concatenate 'string "Hello " "world!") ; => "Hello world!"

一個字符串也可被視做一個字符序列

(elt "Apple" 0) ; => #\A

format被用於格式化字符串

(format nil "~a can be ~a" "strings" "formatted")

利用format打印到屏幕上是很是簡單的(譯者注:注意到第二個參數是t,不一樣於剛剛的nil)~% 表明換行符

(format t "Common Lisp is groovy. Dude.~%")

變量

你能夠經過defparameter建立一個全局(動態)變量。變量名能夠是除了:()[]{}",'`;#|\ 這些字符以外的其餘任何字符。

動態變量名應該由*號開頭與結尾!(譯者注:這個只是一個習慣)

(defparameter *some-var* 5)
*some-var* ; => 5

你也可使用Unicode字符:

(defparameter *AΛB* nil)

訪問一個在以前從未被綁定的變量是一種不規範的行爲(即便依然是可能發生的),不要嘗試那樣作。

局部綁定:在(let ...)語句內,me被綁定到dance with you上。

let老是返回在其做用域內最後一個表達式的值

(let ((me "dance with you"))
  me)
;; => "dance with you"

結構體和集合

結構體

(defstruct dog name breed age)
(defparameter *rover*
    (make-dog :name "rover"
              :breed "collie"
              :age 5))
*rover* ; => #S(DOG :NAME "rover" :BREED "collie" :AGE 5)

(dog-p *rover*) ; => t  ;; ewww)
(dog-name *rover*) ; => "rover"

Dog-pmake-dog,以及dog-name都是由defstruct建立的!

點對單元(Pairs)

cons可用於生成一個點對單元, 利用car以及cdr將分別獲得第一個和第二個元素。

(cons 'SUBJECT 'VERB) ; => '(SUBJECT . VERB)
(car (cons 'SUBJECT 'VERB)) ; => SUBJECT
(cdr (cons 'SUBJECT 'VERB)) ; => VERB

列表

全部列表都是由點對單元構成的「鏈表」。它以'nil'(或者'())做爲列表的最後一個元素。

(cons 1 (cons 2 (cons 3 nil))) ; => '(1 2 3)

list是一個生成列表的便利途徑

(list 1 2 3) ; => '(1 2 3)

而且,一個引用也可被用作字面意義上的列表值

'(1 2 3) ; => '(1 2 3)

一樣的,依然能夠用cons來添加一項到列表的起始位置

(cons 4 '(1 2 3)) ; => '(4 1 2 3)

append也可用於鏈接兩個列表

(append '(1 2) '(3 4)) ; => '(1 2 3 4)

或者使用concatenate

(concatenate 'list '(1 2) '(3 4))

列表是一種很是核心的數據類型,因此有很是多的處理列表的函數,例如:

(mapcar #'1+ '(1 2 3))             ; => '(2 3 4)
(mapcar #'+ '(1 2 3) '(10 20 30))  ; => '(11 22 33)
(remove-if-not #'evenp '(1 2 3 4)) ; => '(2 4)
(every #'evenp '(1 2 3 4))         ; => nil
(some #'oddp '(1 2 3 4))           ; => T
(butlast '(subject verb object))   ; => (SUBJECT VERB)

向量

向量的字面意義是一個定長數組。(譯者注:此處所謂「字面意義」,即指#(......)的形式,下文還會出現)

#(1 2 3) ; => #(1 2 3)

使用concatenate來將兩個向量首尾鏈接在一塊兒

(concatenate 'vector #(1 2 3) #(4 5 6)) ; => #(1 2 3 4 5 6)

數組

向量和字符串只不過是數組的特例

二維數組

(make-array (list 2 2))

;(make-array '(2 2)) 也是能夠的

; => #2A((0 0) (0 0))

(make-array (list 2 2 2))

; => #3A(((0 0) (0 0)) ((0 0) (0 0)))

注意:數組的默認初始值是能夠指定的。下面是如何指定的示例:

(make-array '(2) :initial-element 'unset)

; => #(UNSET UNSET)

若想獲取數組[1][1][1]上的元素:

(aref (make-array (list 2 2 2)) 1 1 1)

; => 0

變長向量

若將變長向量打印出來,那麼它的字面意義上的值和定長向量的是同樣的。

(defparameter *adjvec* (make-array '(3) :initial-contents '(1 2 3)
      :adjustable t :fill-pointer t))

*adjvec* ; => #(1 2 3)

添加新的元素:

(vector-push-extend 4 *adjvec*) ; => 3

*adjvec* ; => #(1 2 3 4)

集合

不怎麼嚴謹地說,集合也可被視爲列表

(set-difference '(1 2 3 4) '(4 5 6 7)) ; => (3 2 1)
(intersection '(1 2 3 4) '(4 5 6 7)) ; => 4
(union '(1 2 3 4) '(4 5 6 7))        ; => (3 2 1 4 5 6 7)
(adjoin 4 '(1 2 3 4))     ; => (1 2 3 4)

然而,你可能想使用一個更好的數據結構,而並不是一個鏈表

哈希表

在Common Lisp中,「字典」和哈希表的實現是同樣的。

建立一個哈希表:

(defparameter *m* (make-hash-table))

給定鍵,設置對應的值

(setf (gethash 'a *m*) 1)

(經過鍵)檢索對應的值

(gethash 'a *m*) ; => 1, t

注意此處有一細節:Common Lisp每每返回多個值。gethash返回的兩個值是t,表明找到了這個元素;返回nil表示沒有找到這個元素。(譯者注:返回的第一個值表示給定的鍵所對應的值或者nil;第二個是一個布爾值,表示在哈希表中是否存在這個給定的鍵。)例如,若是能夠找到給定的鍵所對應的值,則返回一個t,不然返回nil

由給定的鍵檢索一個不存在的值,則返回nil(譯者注:這個nil是第一個nil,第二個nil實際上是指該鍵在哈希表中也不存在)。

(gethash 'd *m*) ;=> nil, nil

給定一個鍵,你能夠指定其對應的默認值:

(gethash 'd *m* :not-found) ; => :NOT-FOUND

在此,讓咱們看一看怎樣處理gethash的多個返回值。

(multiple-value-bind
      (a b)
    (gethash 'd *m*)
  (list a b))
; => (NIL NIL)

(multiple-value-bind
      (a b)
    (gethash 'a *m*)
  (list a b))
; => (1 T)

函數

使用lambda來建立一個匿名函數。一個函數老是返回其形式體內最後一個表達式的值。將一個函數對象打印出來後的形式是多種多樣的...

(lambda () "Hello World") ; => #<FUNCTION (LAMBDA ()) {1004E7818B}>

使用funcall來調用lambda函數:

(funcall (lambda () "Hello World")) ; => "Hello World"

或者使用apply

(apply (lambda () "Hello World") nil) ; => "Hello World"

顯式地定義一個函數(譯者注:即非匿名的)

(defun hello-world ()
   "Hello World")
(hello-world) ; => "Hello World"

剛剛上面函數名"hello-world"後的()實際上是函數的參數列表:

(defun hello (name)
   (format nil "Hello, ~a " name))

(hello "Steve") ; => "Hello, Steve"

函數能夠有可選形參而且其默認值都爲nil

(defun hello (name &optional from)
    (if from
        (format t "Hello, ~a, from ~a" name from)
        (format t "Hello, ~a" name)))

 (hello "Jim" "Alpacas") ;; => Hello, Jim, from Alpacas

你也能夠指定那些可選形參的默認值

(defun hello (name &optional (from "The world"))
   (format t "Hello, ~a, from ~a" name from))

(hello "Steve")
; => Hello, Steve, from The world

(hello "Steve" "the alpacas")
; => Hello, Steve, from the alpacas

固然,你也能夠設置所謂關鍵字形參;關鍵字形參每每比可選形參更具靈活性。

(defun generalized-greeter (name &key (from "the world") (honorific "Mx"))
    (format t "Hello, ~a ~a, from ~a" honorific name from))

(generalized-greeter "Jim")   ; => Hello, Mx Jim, from the world

(generalized-greeter "Jim" :from "the alpacas you met last summer" :honorific "Mr")
; => Hello, Mr Jim, from the alpacas you met last summer

等式

Common Lisp具備一個十分複雜的用於判斷等價的系統,下面只是其中一部分的例子:

若要比較數值是否等價,使用=

(= 3 3.0) ; => t
(= 2 1) ; => nil

若要比較對象的類型,則使用eql (譯者注:抱歉,翻譯水平實在有限,下面是我我的的補充說明: eq 返回真,若是對象的內存地址相等, eql 返回真,若是兩個對象內存地址相等,或者對象的類型相同,而且值相等。例如同爲整形數或浮點數,而且他們的值相等時,兩者eql等價。想要弄清eql,其實有必要先了解eq能夠參考 能夠去CLHS上分別查看二者的文檔。另外,《實用Common Lisp編程》的4.8節也提到了二者的區別。

(eql 3 3) ; => t
(eql 3 3.0) ; => nil
(eql (list 3) (list 3)) ; => nil

對於列表、字符串、以及位向量,使用equal

(equal (list 'a 'b) (list 'a 'b)) ; => t
(equal (list 'a 'b) (list 'b 'a)) ; => nil

控制流

條件判斷語句

(if t                ; 「test」,即判斷語句
    "this is true"   ; 「then」,即判斷條件爲真時求值的表達式
    "this is false") ; 「else」,即判斷條件爲假時求值的表達式
; => "this is true"

在「test」(判斷)語句中,全部非nil或者非()的值都被視爲真值:

(member 'Groucho '(Harpo Groucho Zeppo)) ; => '(GROUCHO ZEPPO)
(if (member 'Groucho '(Harpo Groucho Zeppo))
    'yep
    'nope)
; => 'YEP

cond將一系列測試語句串聯起來,並對相應的表達式求值:

(cond ((> 2 2) (error "wrong!"))
      ((< 2 2) (error "wrong again!"))
      (t 'ok)) ; => 'OK

對於給定值的數據類型,typecase會作出相應地判斷:

(typecase 1
  (string :string)
  (integer :int))

; => :int

迭代

固然,遞歸是確定被支持的:

(defun walker (n)
  (if (zerop n)
      :walked
      (walker (1- n))))

(walker) ; => :walked

而大部分場合下,咱們使用DOLIST或者LOOP來進行迭代

(dolist (i '(1 2 3 4))
  (format t "~a" i))

; => 1234

(loop for i from 0 below 10
      collect i)

; => (0 1 2 3 4 5 6 7 8 9)

可變性

使用setf能夠對一個已經存在的變量進行賦值。事實上,剛剛在哈希表的例子中咱們已經示範過了。

(let ((variable 10))
    (setf variable 2))
 ; => 2

所謂好的Lisp編碼風格就是爲了減小使用破壞性函數,防止發生反作用。

類與對象

咱們就不寫什麼有關動物的類了,下面給出的人力車的類

(defclass human-powered-conveyance ()
  ((velocity
    :accessor velocity
    :initarg :velocity)
   (average-efficiency
    :accessor average-efficiency
   :initarg :average-efficiency))
  (:documentation "A human powered conveyance"))

defclass,後面接類名,以及超類列表。再接着是槽的列表(槽有點像Java裏的成員變量),最後是一些可選的特性,例如文檔說明「:documentation」。

若是超類列表爲空,則默認該類繼承於「standard-object」類(standard-object又是T的子類)。這種默認行爲是能夠改變的,但你最好有必定的基礎而且知道本身到底在幹什麼。參閱《The Art of the Metaobject Protocol》來了解更多信息。

(defclass bicycle (human-powered-conveyance)
  ((wheel-size
    :accessor wheel-size
    :initarg :wheel-size
    :documentation "Diameter of the wheel.")
   (height
    :accessor height
    :initarg :height)))

(defclass recumbent (bicycle)
  ((chain-type
    :accessor chain-type
    :initarg  :chain-type)))

(defclass unicycle (human-powered-conveyance) nil)

(defclass canoe (human-powered-conveyance)
  ((number-of-rowers
    :accessor number-of-rowers
    :initarg :number-of-rowers)))

在REPL中對human-powered-conveyance類調用DESCRIBE後結果以下:

(describe 'human-powered-conveyance)

; COMMON-LISP-USER::HUMAN-POWERED-CONVEYANCE
;  [symbol]
;
; HUMAN-POWERED-CONVEYANCE names the standard-class #<STANDARD-CLASS
;                                                    HUMAN-POWERED-CONVEYANCE>:
;  Documentation:
;    A human powered conveyance
;  Direct superclasses: STANDARD-OBJECT
;  Direct subclasses: UNICYCLE, BICYCLE, CANOE
;  Not yet finalized.
;  Direct slots:
;    VELOCITY
;      Readers: VELOCITY
;      Writers: (SETF VELOCITY)
;    AVERAGE-EFFICIENCY
;      Readers: AVERAGE-EFFICIENCY
;      Writers: (SETF AVERAGE-EFFICIENCY)

注意到這些有用的返回信息——Common Lisp一直是一個交互式的系統。

若要定義一個方法;

注意:咱們計算自行車輪子周長時使用了這樣一個公式:C = d * pi

(defmethod circumference ((object bicycle))
  (* pi (wheel-size object)))

pi在Common Lisp中已是一個內置的常量。

假設咱們已經知道了效率值(「efficiency value」)和船槳數大概呈對數關係。那麼效率值的定義應當在構造器/初始化過程當中就被完成。

下面是一個Common Lisp構造實例時初始化實例的例子:

(defmethod initialize-instance :after ((object canoe) &rest args)
  (setf (average-efficiency object)  (log (1+ (number-of-rowers object)))))

接着初構造一個實例並檢查平均效率...

(average-efficiency (make-instance 'canoe :number-of-rowers 15))
; => 2.7725887

宏可讓你擴展語法

例如,Common Lisp並無自帶WHILE循環——因此讓咱們本身來爲他添加一個;若是按照彙編程序的直覺來看,咱們會這樣寫:

(defmacro while (condition &body body)
    "While `condition` is true, `body` is executed.

`condition` is tested prior to each execution of `body`"
    (let ((block-name (gensym)))
        `(tagbody
           (unless ,condition
               (go ,block-name))
           (progn
           ,@body)
           ,block-name)))

讓咱們來看看它的高級版本:

(defmacro while (condition &body body)
    "While `condition` is true, `body` is executed.

`condition` is tested prior to each execution of `body`"
  `(loop while ,condition
         do
         (progn
            ,@body)))

然而,在一個比較現代化的編譯環境下,這樣的WHILE是沒有必要的。LOOP形式的循環和這個WHILE一樣的好,而且更易於閱讀。

注意反引號`、逗號,以及@這三個符號:

反引號`是一種所謂「quasiquote」的引用類型的運算符,有了它,以後的逗號,纔有意義。

逗號,意味着解除引用(unquote,即開始求值)。

@符號則表示將當前的參數插入到當前整個列表中。

譯者注:要想真正用好、用對這三個符號,須要下一番功夫。甚至光看《實用 Common Lisp 編程》中關於宏的介紹都是不夠的)。建議再去讀一讀Paul Graham的兩本著做《ANSI Common Lisp》和《On Lisp》。

函數gensym建立一個惟一的符號——這個符號確保不會出如今其餘任何地方。這樣作是由於,宏是在編譯期展開的,而在宏中聲明的變量名極有可能和常規代碼中使用的變量名發生衝突。

能夠去《實用 Common Lisp 編程》中閱讀更多有關宏的內容。

拓展閱讀

繼續閱讀《實用 Common Lisp 編程》一書

致謝

很是感謝Scheme社區的人們,我基於他們的成果得以迅速的寫出這篇有關Common Lisp的快速入門

同時也感謝Paul Khuong ,他提出了不少有用的點評。

譯者寄語

祝福那些將思想鑲嵌在重重括號以內的人們。


原文 Learn X in Y minutes Where X=Common Lisp
做者 Paul Nathan
譯者 Mac Davidmut0u

相關文章
相關標籤/搜索