快速導覽
1 Quick Start
1.1 常見的值與表達式
1.2 控制流
1.3 提前中斷
2 模組(Module)
2.1 檔案
2.2 Collection
2.3 專案
2.3.1 測試
2.3.2 Executable
2.3.3 清理
2.4 #lang
3 加上型別(typed/  racket)
3.1 常見的型別
3.2 函數類型
3.3 struct
3.4 union type
3.5 recursive type
3.6 polymorphism
3.7 inst/  ann
3.7.1 ann
3.7.2 inst
3.8 interaction
8.1

快速導覽

Lîm Tsú-thuàn <racket@racket.tw>

1 Quick Start

racket 有一個奇特的怪僻,就是每個檔案都必須標明自己的

#lang xxx

,一開始只要先記得

#lang racket

就可以了。

1.1 常見的值與表達式

racket 作為一個實用的程式語言,自然有熟悉的概念出現,下面就簡單條列一些常見的值與表達式吧:

;;; 註解是用 ; 為開頭
"我是字串" ; 字串
#t ; True
#f ; False
1 ; Integer
3.14 ; Float
4/3 ; Real
3+4i ; Complex
e ; 不是,我就只是叫 e 的變數 XD
 
'(1 2 3) ; Listof Integer,也可以寫
(list 1 2 3) ; 兩邊有微妙的差別但暫時還沒有很重要
 
(λ (x) x) ; lambda,也有人翻譯叫匿名函數,也可以寫成
(lambda (x) x) ; 但在 racket 內建 IDE DrRacket 裡面打出 λ 只需要按 command+\ 非常容易
(define x 1) ; 定義變數
(define (id x) x) ; 定義函數,也可以寫成
(define id (λ (x) x)) ; 但並不會比較好讀 :)
 
(set! x 2) ; 重綁定 x(a.k.a. 賦值)
 
(f a b c) ; 函數呼叫,函數是 f,參數是 a b c

1.2 控制流

作為 Lisp 的主要分支之一,racket 到處都是 () [] 等 S expression,並且稱呼 define 等特殊的表達式為 form。 分辨函數與 form 在 racket 中並不容易,但影響其實不大,因為很少有人會需要弄清楚兩者的差別。 [] 與 () 可以隨意替換使用,這有助於改善可讀性。例如 match 這個用來做 pattern matching 的 form:

(match t
  [(t:1 _ _ _) #t]
  [(t:3 a _ c) (a c)]
  [_ #f])

racket 有數種控制流 form:

(define condition? (= 1 1))
;;; if form 應該是最簡單的,條件、then-expr、else-expr
(if condition?
    1
    2)
; 1
;;; cond 後可以接任意個 cond-clause
; 其中每個 cond-clause 都有 test-expr 或是 else pattern
;  test-expr 成立,就會執行整串 body,就像 number? x 展示的那樣
(define (is x)
  (cond
    [(number? x)
     (displayln "do something else")
     "number"]
    [(string? x) "string"]
    [else "unknown"]))
(is "1")
; "string"
(is 2)
; do something else
; "number"
;;; match 就是 pattern matching
; 下面展示了 match  list  match 時解開 list 得到 head tail 的情況
(define (reduce x [r 0])
  (match x
    ['() r]
    [(cons car cdr) (reduce cdr (+ r car))]))
(reduce '(1 2 3))
; 6

更多資訊

1.3 提前中斷

在 racket 中有兩點需要特別注意:

1. 急切求值,簡單來說就是運算會馬上發生,與 Haskell 成為對比。 2. 沒有 return,也沒有 exception。

等等,看到第二點先別急著說這什麼垃圾 www,雖然沒有內建 return 或是 exception,但 racket 提供了更強大的工具 ◊em{continuation}:

(define (foo x)
  (let/cc return
    (when (string? x)
      (return x))
    1))
(foo 1)
;;; 1
(foo "a")
;;; "a"
(foo 'a)
;;; 1

雖然在這個案例裡面內建 return 的語言更方便,但 continuation 提供了更多功能,不過這邊就不深入介紹,之後再另開文章寫這個 XD。

2 模組(Module)

2.1 檔案

racket 的每一個檔案本身都會是一個模組(module),例如:

;;; hello.rkt
#lang racket
 
(provide print-hello)
 
(define (print-hello)
  (printf "Hello~n"))

對同一個目錄的檔案來說就可以用

(require "hello.rkt")

引用這個模組中的定義。racket 將 require 後的字串解析為相對路徑,所以也可以用 "../xxx.rkt" 等方式引用。

2.2 Collection

Collection 是已安裝的一群模組,引用 Collection 是用沒有檔案類型類後綴的路徑,下面引用了在 Collection "racket" 中的 module "date.rkt"

(require racket/date)
 
(printf "Today is ~s\n"
        (date->string (seconds->date (current-seconds))))

除了內建的 Collection,可以用 raco pkg 指令取得第三方程式庫:

raco pkg install cur

2.3 專案

可以用以下指令生成新的專案(開發新的 Collection):

raco pkg new <collection-name>
raco pkg install --auto

注意 install –auto 那行不能省略,不然會看到 test 指令瘋狂失敗(笑)。

2.3.1 測試

測試在實際的工作上非常有用,而 Racket 也支援這樣的功能,我們在任意一個專案中的檔案都可以寫測試。首先需要宣告一個特殊的模組,用 module+ 這個特殊的 form 來宣告一個 submodule,並引入 rackunit 這個單元測試框架:

(module+ test
  (require rackunit))
 
(module+ test
  (check-eq? 1 2))

submodule 可以重複宣告,對我們來說只要知道最後會被認為是同一個 submodule 就可以了,因此我們也可以寫成

(module+ test
  (require rackunit)
 
  (check-eq? 1 2))

然後用指令 raco test . 執行測試。

2.3.2 Executable

如果要開發 executable,在 main.rkt(生成的專案裡會有) 裡的 module+ main 裡面寫的程式就是 executable 會執行的東西,另外引用這個 collection 並不會執行到這些程式。可以用指令 racket -l <collection-name> 來執行 collection。

2.3.3 清理

如果未來刪除這個 collection 目錄,也要記得刪除 raco 中的紀錄:

raco pkg remove <collection-name>

不然就會造成 raco 在其他專案使用時一直找不到該 collection 而沒辦法正常運作。

2.4 #lang

#lang 是 racket 的語言核心,這是 racket 裡面唯一不可以拿掉的部分,事實上 #lang xxx 代表的是以 xxx 包裹這個模組的意思。語法僅僅是表象這個說法在 racket 中發揮的淋漓盡致,最常見的 module language 就是 racket、racket/base。例如說我們可以在 REPL 裡面打:

Examples:
> (module f racket
    (provide (except-out (all-from-out racket) lambda)
             (rename-out [lambda function])))
> (module use 'f
    ((function (x) x) 1))
> (require 'use)

1

暫時不用理解 provide 裡面 except-out rename-out 等等奇怪的東西。我們只關心 module 語法,它接受 name 以及一個可選參數 module。這個 module provide 的 form 會成為該 module 的語言基礎。而 #lang 也只是 module 的語法糖。

#lang s-exp "html.rkt"
 
(title "Queen of Diamonds")
(p "Updated: " ,(now))

如果有 html.rkt 這個 module,s-exp "html.rkt" 就是 (module <file-name> "html.rkt") 而已。

3 加上型別(typed/racket)

雖說 Racket 之中語法並沒有很重要,但還是需要認識裡面常用的 dialect,這篇就是要介紹 typed/racket 這個 dialect。typed/racket 顧名思義就是標註了 type 的 racket,與 racket/base 的語法基本相通,但加入了一些型別標註(annotation)的語法以及給予某些語法變體來幫助使用者寫出更簡潔的程式(雖然這還蠻看人的那些語法)。

3.1 常見的型別

我們可以先來觀察說到底有哪些常見 type 可以用:

#lang typed/racket
 
;  : Type
"string" : String
#\a : Char
#t : Boolean [more precisely: True]
#f : False
'f : Symbol [more precisely: 'f]
0 : Integer [more precisely: Zero]
1 : Integer [more precisely: Positive-Byte]

可以看到一個奇特現象是 racket 的 type 經常有所謂的 more precisely 的標記,來說明存在更精確的型別存在,實際上怎麼做到的這裡不提,但這麼做的理由是為了支持所謂的 type refinement,可以讓 type checker 根據需要限縮型別。除此之外所有的值都可以是自己的 type: (ann 1 1) 是合法的標註。

3.2 函數類型

我們用 -> 這個 type constructor 建構函數類型,-> 在數學上的意思是蘊含,A->B 代表 A 蘊含 B,racket 裡照慣例用了前綴表達法 (-> A B),只要 A B 都是類型,則 (-> A B) 是類型。所以下列都是合法的類型:

(-> Number Number Number)
(-> String String Boolean)

對應 define,typed/racket 提供了 : 作為宣告型別的語法:

(: add (-> Number Number Number))
(define (add x y)
  (+ x y))
(: same (-> String String Boolean))
(define (same s1 s2)
  (eqv? s1 s2))

然而也提供了修改過的 define

(define (add [x : Number]
             [y : Number])
  : Number
  (+ x y))
(define (same [s1 : String]
              [s2 : String])
  : Boolean
  (eqv? s1 s2))

這些語法可以大部分的情況了,但 racket 本身允許可選參數的存在,在 typed/racket 中就對應了 ->* 這個 type constructor:

(: eval (->* (Term) (Env) Value))
(define (eval term [env '()])
  ;;; ignore
  )

p.s. 根據我目前所知,->* 必須明確的寫成 declare/define 分開的形式,寫成 define 內涵型別定義的 form 時 type checker 還是會覺得 optional argument 沒被填上導致 type mismatching。

而 racket 本來就支援 case-lambda(aka function overloading),所以 typed/racket 也需要處理這個情況:

(: append2 (All (a) (case->
                      [(Listof a) a -> (Listof a)]
                      [(Listof a) (Listof a) -> (Listof a)])))
;;; 直接定義
(define append2
  (case-lambda #:forall (a)
    [([l : (Listof a)]
      [x : a])
     (append l (list x))]
    [([l1 : (Listof a)]
      [l2 : (Listof a)])
     (append l1 l2)]))

只有在 require/typed 的時候能用,不然參數相同的情況下 typed/racket 會把 x 推導成 (U (Listof a) a)

有趣(麻煩)的最後一種參數是 keyword argument:

(: position (->* (#:line Integer #:column Integer #:filename String) (#:msg String) Position))
(define (position #:line line #:column column #:filename filename #:msg [msg ""])
  ;;; ignore
  )

一般形式的 keyword argument 應該不是什麼大問題,但 optional keyword argument 要注意 pre-binding 不能把 keyword 自己綁進去,而是要把它的對應變數包進去,一如 #:msg [msg ""] 所示。

3.3 struct

struct 必定會引入新的型別,並且使用 nominal subtyping,下面提供一個 struct 的簡單案例:

(struct point
  ([x : Real]
   [y : Real]))

現在表達式 (point 1 2) 的型別就會是 pointstruct 可以有 super type:

(struct dog animal ())

這樣 dog 也可以是 animal,因為 animaldog 的 super type。

3.4 union type

為了讓許多原先存在於 racket 的概念運作,也是為了更複雜的應用,typed/racket 提供了 union type,語法 (U a b c) 代表 這個型別可能是 abc

(let ([n 10])
  (if (even? n)
    'is-even
    'is-odd))

這個表達式的型別就是 Symbol [more precisely: (U 'is-even 'is-odd)] (值本身是自己的型別)

3.5 recursive type

type 之間互相參照就叫做 recursive type,在 typed/racket 裡可以用 Udefine-type 來達成:

(define-type BinaryTree (U Number (Pair BinaryTree BinaryTree)))
 
(define-type Tree (U leaf node))
(struct leaf ([val : Number]))
(struct node ([left : Tree] [right : Tree]))

當然我們不可以直接參照自己:

(define-type A A)
(define-type B (U Number B))
; 以上都是 invalid type。

3.6 polymorphism

polymorphism 或是有些人只聽過 generic,我不打算分清楚他們的差別,以免讀者陷在裡面,這裏主要說的是參數多型,與 struct 那邊的 super type 不同,現在先來看一個簡單的範例:

(define-type (Opt a) (U None (Some a)))
(struct None ())
(struct (a) Some ([v : a]))

這個型別可以用來表達可能有值也可能沒有值的情況。函數一樣可以接受不定型別作為參數:

(: list-length (All (A) (-> (Listof A) Integer)))
(define (list-length lst)
  (if (null? lst)
    0
    (+ 1 (list-length (cdr lst)))))

All 對應邏輯裡面的 符號,意思是對所有 A 都成立。

3.7 inst/ann

3.7.1 ann

ann 是 annotation 的縮寫,用來標記表達式 (expression) 的型別是什麼,總計有三種寫法:

(let ([#{x : Number} 7]) x)
(ann x Number)
#:{x :: Number}

由於 typed/racket 有 precisely type,所以用 annotation 有時候是有需要的(笑)。

3.7.2 inst

inst 就更重要了,為了基於 racket 許多內建的 case-lambda 跟 polymorphism function 上,有時候會遇到 type checker 沒辦法推導出正確型別的情況,這時候就需要 inst 提供 type argument 來實例化 type:

;;; 這會撞到 Polymorphic function `foldl' could not be applied to arguments
(foldl cons null (list 1 2 3 4))
;;; 解法
(foldl (cons Integer Integer) null (list 1 2 3 4))

3.8 interaction

使用 typed/racket,如果函式庫作者沒有提供 type definition 難道就沒救了嗎?這就是最後一塊拼圖,typed/racket 允許使用者提供型別定義:

(require/typed "point.rkt"
  [#:struct point ([x : Real] [y : Real])]
  [distance (-> point point Real)])

這樣一來使用者就不需要綁死在 racket 或是 typed/racket 上,而是能夠按需要選擇適合的語言了。