程式入門教學
1 入門
1.1 何謂程式(program)?
1.2 何謂運算(computation)?
1.3 何謂抽象(abstraction)?
1.4 寫程式這份工作
1.5 程式
2 資料(Data)
2.1 常見資料
2.1.1 數字
2.1.1.1 整數
2.1.1.2 有理數
2.1.1.3 浮點數
2.1.1.4 複數
2.1.1.5 exercise
2.1.2 布林值(Boolean)
2.1.3 文字
2.1.3.1 字元
2.1.3.2 字串
2.2 Racket 特有的資料
2.2.1 Symbol
2.2.2 Keyword
2.2.3 Pair 與 List
2.2.3.1 exercise
2.3 更複雜的資料
2.3.1 Vector
2.3.2 Hash Table
2.3.3 Box
2.3.4 Void and Undefined
3 語法與計算規則
3.1 變數與函數(variable and function)
3.1.1 exercise
3.2 conditional:   if/  cond/  case/  match
3.2.1 if
3.2.2 cond
3.2.3 case
3.2.4 match
3.3 let/  let*/  letrec
4 軟體開發
4.1 撰寫好程式
4.1.1 格式
4.1.1.1 避免過長的單行
4.1.1.2 不要拆散結尾括號
4.1.1.3 子表達式的縮排要一致
4.1.1.4 折一個就全部折
4.1.2 共通的最佳實踐
4.1.2.1 慎重的命名
4.1.2.2 為程式加上註解
4.1.2.3 保持函數簡短
4.1.2.4 避免重複的程式
4.1.2.5 避免重複的計算
4.1.2.6 使用高階函數
4.1.3 測試
4.1.4 模組
4.2 閱讀好程式
4.3 發佈好程式
8.1

程式入門教學

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

以下教學假設您已經成功安裝 Racket,並理解簡單的數學,擁有相當的生活常識。教學在第一次提到名詞時會中英並陳,之後則根據需要使用中或英,在翻譯會造成理解上的困擾時不翻。

1 入門

1.1 何謂程式(program)?

程式就是一串說明如何執行運算的指令,那麼無可避面的我們會有進一步的問題:何謂運算?

1.2 何謂運算(computation)?

運算可以是各種各樣的東西,1+1 是運算嗎?是的,1+1 當然是一種運算,並且我們知道它的結果是 2。事實上,判斷也是一種運算,1+1 = 2 的等於生成真(true)或假(false)作為結果。根據長年來學習數學的經驗,我相信大多數人可以回答 2+3 = 5 為真。但為什麼 2+3=5 呢?因為我們約定好了 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 等「符號」組成的東西叫做「數字」,且說好了 + 這個符號應該做出什麼樣的「運算」。換句話說,雖然運算中間發生了什麼我們不關心,但我們可以通過約定好的「運算規則」知道其結果。

根據我們選定的抽象層面,我們也有可能可以關注到本來無法觀察的運算過程。舉個例子,方才我們描述的東西叫做自然數加法,皮亞諾公理(Peano axioms)可以幫助我們觀察某方面的真實。

皮亞諾公理規定了自然數可以由兩個方式得到

剩下的 axioms 是關於 equality 的

上面的形式化裡面可以只讀懂前兩條就好,剩下的公理都是為了保證這跟我們直覺理解到的自然數真的是同一個東西而定義的,對接下來要說明的事物沒有關係。自然數常見的爭論是 0 算不算自然數,不過為了 0+n = n 的特性(稱為 additive identity),我們選擇了 0 是自然數的這邊。歸納公理被省略,不過你可以認為這條公理的重點是為了說明我們在討論合法的函數(對所有輸入皆有輸出,又稱 totality)。

於是我們可以開始討論為何這些允許我們更仔細的觀察加法,我們把加法定義為

這是遞迴定義,但我們知道無論 m 為何,最終運算都會結束(因為 m 會歸零)。不過我們可以觀察到此運算的性質為「加法的結果是兩數中其中一個的減一加另一數再加一」或「兩數之一為零則結果為另一數」。

作為練習,你可以試著手動一步步展開 succ(succ(0)) + succ(succ(0)) 並檢查結果是否是 succ(succ(succ(succ(0))))。

這是個重要的觀察,重點不是加法的另一種定義方式,而是通過轉換觀點可以用各種角度觀察運算的不同「真實」。而這些取決於我們要站在什麼地方理解他們,因此我們也可以說「交易」是一種運算,由可以收錢跟可以付錢的客體完成,其重點是付款金額與收款金額應該相同,我們可以基於剛才的數字加減法完成金額的計算。或是選擇某金額的所有權由付款方移到收款方這樣的方式定義我們的運算。第二種方式看似有點多餘,但其實引入了我們可以在未來追蹤這筆交易的機會,種種計算方式往往沒有優劣之分。重點在衡量什麼是「合適」的層級,這往往需要知道我們要完成什麼以及我們有哪些限制需要遵守,並不僅僅只是運算問題。

現在讓我們面對抽象是什麼這個問題。

1.3 何謂抽象(abstraction)?

抽象是我們整天都在做的事情,例如我現在不擔心我現在使用的編輯軟體為什麼可以動,因為我的目的是要把文章寫出來(除非我的編輯軟體一直當掉)。而我們也不需要擔心為什麼超市買得到食物(除非買不到了)等等,因為我們注意力有限,因而一次只能注意少數事務來進行工作,把關心的部分拉出來的行為就是抽象。

1.4 寫程式這份工作

在說明運算時,我說問題有時不只是運算,這是因為程式涉及到解決問題,因此我們需要

,偶爾我們也需要進行一些科學實驗:

這些是我們設計程式的種種過程與不同面向,經常涉及到關於人的問題,例如老闆覺得重寫系統不夠好玩,怎麼不順便放點新功能呢?不幸的是即便現實世界無比複雜我們還是得找出方式解決問題,萬幸的是即便解決不了問題,我們通常也能通過解決人來收尾(誤)。

在瞎扯這麼多之後,我們可以來看程式到底應該有些什麼。

1.5 程式

很多人誤以為寫程式很難,或是「寫」程式非常重要,這些其實都是錯誤的觀念。不過事實上,「寫」程式是非常簡單的,只要我們不求這些程式可以解決我們關心的問題的話,例如下面這段程式(看不懂也沒有關係,之後會再做介紹)

#lang racket/base
 
(displayln "hello, world")

隨便建立一個檔案,像是 a.rkt 把程式貼進去,用 racket a.rkt 執行就可以看到印出了 hello, world 這串字。而接下來也是程式

#lang racket/base
 
(displayln "hello, world")
(displayln "hello, world")
(displayln "hello, world")

事實上我們要重複幾行 (displayln "hello, world") 都無所謂,這些都是程式,只是沒有解決我們可能關心的問題而已。我們關心的是問題有沒有被解決,寫程式只是其中一種辦法而不是最重要的部分。而程式具備的重要特性之一,不單是可以執行,而是可以被閱讀。一個方法可以通過被閱讀理解後傳承,稍作修改就能解決新的問題,或是在這之上解決更複雜的問題,不需要反覆手動執行一次流程,才是程式的價值所在。

既然程式可以被書寫閱讀(雖然是通過鍵盤與螢幕),那麼它就需要有符號、文法與語義,各種語言的細節常常在此愚弄初來乍到者,誤以為程式語言俱是龐然大物,然而,程式語言該有的核心其實亦常簡單:

輸入輸出是很容易理解的,無論如何最終程式都是為了人們製作的,所以會有各式各樣的輸入輸出裝置存在,例如鍵盤、滑鼠、VR 眼鏡、螢幕、開關等等。數學運算很不幸的沒有好的解釋,不過簡單數學在很多問題裡面都會用到,或許是個理解它為何存在的方向。條件執行是因為我們經常需要根據狀況做出不同的行為,例如掃地機器人撞到東西時會轉向等。重複執行的使用方式變化多端,不過大抵上來說是因為我們希望可以描述「一直做到完成」這類型的抽象,否則我們就得苦哈哈的坐在那邊不斷重新執行程式直到滿足某個條件為止。

現在你應該已經對程式能夠與不能夠完成的事情有了一點概念,也了解到運算跟抽象定義的威力,然而我們終究還是要學習一門實際的語言來進行寫程式這個行為,因此接下來轉入介紹 Racket 這個程式語言。

2 資料(Data)

2.1 常見資料

不管是哪一種程式都會進行計算,而計算就會得到結果,結果以人類可以理解的方式呈現。為此大多數語言都設立了一些常用的資料表達方式,Racket 也不例外的支援了

下面我們分數字、布林值跟文字來討論。在 terminal 裡面直接輸入 racket 會出現一個可以輸入運算式(expression)的文字交互環境,我們一般叫它 REPL,下面的程式是以含輸入提示符號(>)排列的。啟動 REPL 的畫面大概會長得像下面那樣(我從印出來的文字裡面拿掉版本資訊,不過執行之後應該就看得出自己有沒有弄對了)。

$ racket
Welcome to Racket
>
2.1.1 數字
2.1.1.1 整數

整數一如常見的理解,

> 1

1

> 1232131245

1232131245

> -99230193

-99230193

都是整數

2.1.1.2 有理數

Racket 亦支援有理數如

> 3/4

3/4

> 16066/207275633

16066/207275633

,而且它甚至會自動約分(笑)

2.1.1.3 浮點數

就像前面提過的,不可數集合沒辦法建構出來,所以用浮點數這種對於實數的近似值數值表現法來處理,寫成

> 2.4214

2.4214

> 8.34155152

8.34155152

等,也有支援常用的特殊值的近似,如

> (require racket/math)
> pi

3.141592653589793

2.1.1.4 複數

除開實數是採用浮點數近似,複數跟在高中學習到的東西基本上是一樣的

> 5+2i

5+2i

> 10.0+3.412i

10.0+3.412i

2.1.1.5 exercise

現在讓我們實際使用數字看看

2.1.2 布林值(Boolean)

布林值也是現代人熟知的資料之一了,用於真假運算上,在 Racket 中寫法比較獨特

> "#t"

"#t"

> "\n"

"\n"

> "#f"

"#f"

大寫不改變其意

> "#T"

"#T"

> "\n"

"\n"

> "#F"

"#F"

2.1.3 文字
2.1.3.1 字元

字元用 #\ 作為前綴,在後面接上想要的字元就是該字元

> #\c

#\c

> #\a

#\a

> #\0

#\0

> #\\

#\\

,而 Racket 也支援一些控制字元(鍵盤上的特殊功能鍵)如

> #\return

#\return

> #\tab

#\tab

2.1.3.2 字串

現在可以來說明為什麼需要 quote 了,假設我們需要一串字叫 hello,我們的語言就寫

hello

所以列印 hello 寫成

(display hello)

可是變數也是

hello

那我們怎麼知道

(display hello)

hello 到底是什麼?乃至 (display hello) 到底是變數還是字串?所以實際上我們用 " 把字串包起來,寫成

(display "hello")

但我們用了 " 之後,要怎麼在字串裡面表示 "?所以有所謂的轉義字符(escape character),例如 "hello" 就可以寫成

"\"hello\""

2.2 Racket 特有的資料

雖說是特有概念但其實很多概念是繼承自 Lisp/Scheme 的,包含

2.2.1 Symbol

前面加了 ' 的 identifier 就會生成 Symbol 的值(所以也可以說 symbol 就是 quoted identifier),例如:

> 'a

'a

> (symbol? 'a)

#t

第二句 (symbol? 'a) 可以讀作:'a 是 symbol 嗎?

我們還可以詢問兩個 symbol 是否相等:

> (eq? 'a 'a)

#t

> (eq? 'a 'abc)

#f

> (eq? 'a 'ABC)

#f

還可以把字串轉換成 symbol:

> (string->symbol "a")

'a

除了以下字元跟空白符號之外的字元都是合法的 identifier 字元(合法在這裡指可以出現在 identifier 內):

( ) [ ] { } " , ' ` ; # | \

除此之外

而如果真的想要包含非法字元,可以用 | 包起來或是用 \ 前綴逃脫,所以以下都是合法的 symbol

2.2.2 Keyword

Keyword 跟 Symbol 很像,只是前綴是 #:,例如:

> '#:apple

'#:apple

> (eq? '#:apple (string->keyword "apple"))

#t

keyword 主要是用在命名參數(或是帶名參數)上,讀者暫時可以先不用管這是什麼意思,講到函數(function)時會更深入一點介紹命名參數是什麼。這裏先讓讀者看 with-output-to-file 怎麼用到 keyword 的:

(with-output-to-file
  (build-path (current-directory) "stuff.txt")
  (lambda () (printf "example\n"))
  #:mode 'text
  #:exists 'replace)
2.2.3 Pair 與 List

Pair 與 List 是 Racket 中的程式、也是資料,這種對應使得我們有能力操作製作程式的程式,但現在我們不需要關心這個進階功能。首先我們看一些 Pair 的實例:

> '(1 . 2)

'(1 . 2)

> '(a . b)

'(a . b)

> '(a b . c)

'(a b . c)

事實上 '(a b . c)'(a . (b . c)) 的簡寫。因此我們可以說 Pair 就是擁有兩個「內容」的結構,內容可以是任何資料。我們可以用 car(取得左邊)和 cdr(取得右邊)存取 Pair 的內容,因此:

> (car '(a . b))

'a

> (cdr '(a . b))

'b

> (cdr '(a b . c))

'(b . c)

List 是特殊的 Pair,當一個 Pair 的右側是空的 Pair () 時,該 Pair 就是一個 List,並且空 Pair 本身也是 List:

> (list? '())

#t

> (cdr '(a))

'()

> (cddr '(a b))

'()

有解構的方式,Pair 也有建構的方式,字面值語法 (1 . 2) 無法和函數呼叫(之後才會提到)語法區隔,因此我們得寫成 '(1 . 2),但我們也可以改用其建構函數 cons

> (cons 1 2)

'(1 . 2)

> (cons 'a 'b)

'(a . b)

> (cons 'a (cons 'b 'c))

'(a b . c)

到此我們已經對 Pair 和 List 有足夠的了解,練習看看吧

2.2.3.1 exercise

2.3 更複雜的資料

這節是講述一些相對複雜的資料,只是留作參考,對學習的影響不大,讀者可以跳到計算規則的部分。

2.3.1 Vector
2.3.2 Hash Table
2.3.3 Box
2.3.4 Void and Undefined

3 語法與計算規則

語法(syntax)和計算規則(computation rule)經常會被混為一談,但他們是不同的,例如一個 Forth 的函數呼叫可能寫成

1 2 3 foo

Racket 寫成

(foo 1 2 3)

但計算規則卻是一樣的:1 2 3foo 的參數,結果是由 foo 的內容決定。於是我們大致可以理解計算規則跟語法為什麼不盡然有關聯了,其實還有很多細節,但對一個 Racket 的入門教學這樣暫時也就夠了。

3.1 變數與函數(variable and function)

函數呼叫(function call)大概是最普遍的計算規則,在各種語言裡面都可以看到,它也經常被稱為 application,因此函數也可以被稱為 applicable。在 Racket 裡,application 就是一個 list,第一個元素被當成函數(所以如果放入不是函數的東西會出現錯誤),剩下的被當成參數。到這裡,Racket 的計算規則暫時可以理解成被 quote 包含的會整塊被當成一個值,剩下的會被當成 application。

有了函數呼叫,自然有定義函數的方式,Racket 提供了 define form 來做這件事

> (define (fib n)
    (case n
      [(0) 1]
      [(1) 1]
      [else (+ (fib (- n 1)) (fib (- n 2)))]))
> (fib 1)

1

> (fib 2)

2

> (fib 3)

3

> (fib 4)

5

> (fib 5)

8

我們也可以把 fib 改寫成 define 的變數形式

(define fib
  (lambda (n)
    (case n
      [(0) 1]
      [(1) 1]
      [else (+ (fib (- n 1)) (fib (- n 2)))])))

到這裡,我們知道 Racket 除了 quote 跟 application 的第三種規則:form。內建的 form 有很多,而我們可以用 define-syntax 等 form 定義更多 form,這就是為什麼 Racket 要讓資料跟程式具有同像性,因為如此一來處理程式就如處理資料一般容易。

接下來,我們來了解更多常見的 form 吧!

3.1.1 exercise

下面是關於 define 的練習

3.2 conditional: if/cond/case/match

3.2.1 if

if form 是各語言常見的語法,以下是一個案例

(if (= x 1)
'x-is-one
'x-is-not-one)

syntax

(if test-expr then-expr else-expr)

我們可以看到 if 有三大元素:

也就是

唯一需要特別注意的是 racket 的 if 不能省略 else-expr(跟某些語言不一樣)。不過如果有這種需要,可以改用 when 或是 unless form。

3.2.2 cond

syntax

(cond cond-clause ...)

 
cond-clause = [test-expr then-body ...+]
  | [else then-body ...+]
  | [test-expr => proc-expr]
  | [test-expr]
cond form 可以看作是 if 的推廣。例子如下:

(cond
[(= x 1) 'x-is-one]
[(= x 2) 'x-is-two]
[(> x 2) 'x-is-more-than-two]
[else    'something-else]) ; else 是可選的

它按順序測試任意數量的 test-expr (i.e. (= x 1)),每個 test-expr 又對應一個表達式 (e.g. 'x-is-one),每組這樣的 sexp 稱為一個 cond-clause (e.g. [(= x 1) 'x-is-one])。最後一個 cond-clause 中 test-case 可換成 else 來匹配任意情況。

改寫成 if form 的形式相當於

(if (= x 1)
'x-is-one
(if (= x 2)
'x-is-two
(if (> x 2) ;  test-case  else 去掉的話,這行應該就是 (when (> x 2) 'x-is-more-than-two)
'x-is-more-than-two
'something-else)))

[test-expr then-body ...+]

一般的 clause 會在 test-expr 為 #true 時執行

[else then-body ...+]

else 是特殊的一個 clause,它表示預設的處理邏輯

[test-expr => proc-expr]
[test-expr]

3.2.3 case

syntax

(case val-expr case-clause ...)

 
case-clause = [(datum ...) then-body ...+]
  | [else then-body ...+]
case form 跟 C 語言的 switch case 語法很像,例子如下:

(case x
[(1)   'x-is-one]
[(2 3) 'x-is-two-or-three]
[else  'something-else]) ; else 是可選的

不妨把 x 位置的表達式稱為 target。方括號的 sexp 稱為 case-clause(e.g. [(2 3) 'x-is-two-or-three]),case-clause 中左手邊的則是一個列表(e.g. '(2 3) 或者 (list 2 3)),檢查 x 是否在列表中,返回對應的表達式(e.g. 'x-is-two-or-three),不考慮性能的情況下改寫成 cond form 的形式相當於

(cond
[(or (equal? x 1))              'x-is-one]
[(or (equal? x 2) (equal? x 3)) 'x-is-two-or-three] ; Racket 會把這行優化成 O(log N)
[else                           'something-else])
3.2.4 match

syntax

(match val-expr clause ...)

 
clause = [pat body ...+]
  | [pat (=> id) body ...+]
  | [pat #:when cond-expr body ...+]
match form 跟剛剛的 case form 長很十分像。只是 target 匹配的不是列表中的元素,而是 pattern。例子如下:

pattern 可以是 constructor 和字面值的組合。
> (match 3
    [1 'target-is-one]
    [2 'target-is-two]
    [3 'target-is-three])

'target-is-three

> (match '(a b c)
    ['(a a a)     "target is a list of three a"]
    [(list a b)   "target is a list of a and b"]
    ['(a b c)     "target is a list of a, b and c"])

"target is a list of a, b and c"

> (match '(a . b)
    [(list a b) 'list]
    [(cons a b) 'pair])

'pair

pattern 可以是 identifier 或 _。當 pattern 是 identifier 或 _ 時,匹配任意值。
> (define x 0)
> (define y 1)
> (match x
    [10 'x-is-ten]
    [_  'x-is-not-ten])

'x-is-not-ten

> (match y
    [x "pattern variable is not the same x as the one we defined at begining"]
    [1 "y is one"])

"pattern variable is not the same x as the one we defined at begining"

> (match 2
    [0 "zero"]
    [1 "one"]
    [x (format "mismatch with value ~s" x)])

"mismatch with value 2"

pattern 可以是 constructor 和 pattern 的組合(nested)。
> (match '(a b)
    [(list 'a x) (format "the second element is ~s" x)]
    [(list  a b) "match but not reach"])

"the second element is b"

> (match '(a b)
    ['(a x)  "(list 'a 'b) != (list 'a 'x)"]
    ['(a b)  "(list 'a 'b) == (list 'a 'b)"])

"(list 'a 'b) == (list 'a 'b)"

> (match '(a (b (c d)))
    [(list 'a (list 'b res)) res])

'(c d)

pattern 中 某個 sub-pattern 的右方插入 ... 代表該 sub-pattern 可以有任意多個。
> (match '(1 1 1)
    [(list 1 ...) 'ones]
    [_ 'other])

'ones

> (match '(1 1 2)
    [(list 1 ...) 'ones]
    [_ 'other])

'other

> (match '(1 2 3 4)
    [(list 1 x ... 4) x])

'(2 3)

> (match (list (list 'a 23 'b) (list 'c 22 'd))
    [(list (list x y z) ...) (apply + y)])

45

3.3 let/let*/letrec

TODO

4 軟體開發

這一篇章的重點不在語言,而在學習如同 racket 熟手一般的真正開發的技巧

4.1 撰寫好程式

這個章節討論 racket 常見的慣例、風格、傳統與陷阱,學習 racket 程式的最佳實踐。

4.1.1 格式
4.1.1.1 避免過長的單行
(define (filter pred? lst)
  (cond
    [(null? lst) '()]
    [(pred? (car lst)) (cons (car lst) (filter pred? (cdr lst)))]
    [else (filter pred? (cdr lst))]))

適當的折行有助於閱讀

(define (filter pred? lst)
  (cond
    [(null? lst)
     '()]
    [(pred? (car lst))
     (cons (car lst)
           (filter pred? (cdr lst)))]
    [else
     (filter pred? (cdr lst))]))
4.1.1.2 不要拆散結尾括號

以下的程式是非常好的 racket 風格

(define (factorial n)
    (if (zero? n)
        1
        (* n (factorial (- n 1)))))

不要寫成

(define (factorial n)
    (if (zero? n)
        1
        (* n (factorial (- n 1)))
    )
)
4.1.1.3 子表達式的縮排要一致

以下是好的案例

(list (foo)
      (bar)
      (baz))

以下是例外,當子表達式是 body 時,我們給它兩個空格做縮排

(let ((pi 3.14)
      (r 120))
  (* pi r r))
4.1.1.4 折一個就全部折

假設你有

(+ 1 foo bar baz)

折行時就應該寫

(+ 1
   foo
   bar
   baz)
4.1.2 共通的最佳實踐

好程式不僅僅是縮排正確而已,還有一些守則需要遵循。

4.1.2.1 慎重的命名

好的命名可能比你想象的還要更重要也更困難。命名應該要含有變數或是函數所做的事情,並且要考慮其被使用的上下文中是否適合。除了命名要含有資訊之外,racket 還有一些慣例可以幫助你的程式變得更易讀,例如回傳 boolean 的函數可以用 ? 結尾,有副作用的函數用 ! 結尾。

4.1.2.2 為程式加上註解

沒有註解勝過錯誤的註解、有註解勝過沒有註解、簡短的註解勝過冗長的註解。

4.1.2.3 保持函數簡短

一個函數應該盡可能保持在一個螢幕可以看完整個定義的大小,大約在 60 行左右,拆分更多函數有助於保持這點。

4.1.2.4 避免重複的程式

把重複的程式抽成函數是個好習慣。

4.1.2.5 避免重複的計算

善用快取避免重複的計算。

4.1.2.6 使用高階函數

用高階函數如 filtermap 可以減少很多重複的基本工作,並且具備相當的描述能力。

4.1.3 測試

在 Racket 裡編寫測試只需要在檔案中寫下:

(module+ test
  (require rackunit)
 
  (check-equal? actual expected))

就可以了,用 raco test path/to.rkt 就可以看到測試是否有通過

4.1.4 模組

Racket 中每個檔案都是一個模組,其中還可以用 module 定義子模組(就像上一節提到的 (module+ test))。每個 collection 自帶一個 info.rkt、一個 main.rkt,collection 本身被飲用時視為一個模組,而其中的檔案就被視為 collection 的子模組。

Racket 的風格是保持精簡,無論是模組、類別、函數或是方法。一萬行的模組就太多了、一千行免強可以接受、五百行內是適合的大小。

模組有適當的組織方式,應當按照:匯出(provide)、引入(require)、定義,這樣的順序編寫。

如果 collection 是一個可執行程式,那麼可以在 main.rkt 中寫:

(module+ main
  )

這樣一來執行:racket -l collection-name 時,其中的內容就會被執行。

4.2 閱讀好程式

帶你看各種優秀的 racket 專案,學習自己發掘程式的設計。

4.3 發佈好程式

討論如何發佈寫好的程式庫。