感謝華盛頓大學 Dan Grossman 老師 以及 Coursera 。
碎言碎語
-
這只是 Programming Languages 這門課程第一部分,在 Part A 中通過 Standard ML 這門編程語言把函數式編程的美妙娓娓道來。在 Part B 以及 Part C 中分別會學習 Racket 以及 Ruby 。
-
這是一門什么樣的課呢?首先,它很好玩,雖然我學過 Scala
學過一些 Scheme,但還是能夠感覺到這門語言的簡潔和優雅。但是要記住,這門課最重要的不是教你學會 Standard ML 這門語言,而是函數式編程中的 immutable data 、function closures 、first-class function、higher-order function,以及這門語言中的 pattern matching、currying and partial application、type inference 等等(當然並不是這門語言所特有的,這些特性在 Scala 中都存在)。不僅僅是這些實際的東西,在 Part A 的 week 4 最后有幾乎一小時的視頻介紹了這門課程的動機:為什么要學習函數式編程?為什么選擇這三門語言? -
為什么我會去學習這門課呢?首先這是華盛頓大學的一門 CS 課程,在網絡上的評價很高,它的課程質量和作業質量得到了保證。再看看這門課程,它一共使用了三門編程語言,既有函數式編程也有面向對象編程,它並不會試圖告訴你這門語言的全部,而是專注於語言特性,一些使得這門語言變得有力和優雅的語言特性,對於一門陌生的語言,這門課程首先會引導我們看見它、使用它,然后再盡可能地作出解釋,而不僅僅是停留在使用的階段。
-
作業的形式是編程題目,有自動打分系統,如果額外的 Challenge Problems 以及 Extra Practice Problems 都能完成的話,還是要花不少時間的。完成要求的題目並不難,思考最簡潔、最優雅的代碼才是最重要的。
-
筆記其實是沒多少的,為什么呢?課程中給出的 Reading Notes 真是干貨滿滿,值得反復閱讀,它提煉了老師的講課 ppt,不過 20 頁的 pdf ,內容卻充實的很,而又往往前后呼應,情節跌宕起伏,
怎么像是說小說一樣。舉個例子,它的標題是這樣的:By Name vs. By Position, Syntactic Sugar, and The Truth About Tuples ,是不是很容易有種一探究竟的沖動呢。 -
這門課上到現在直觀的感受就是:循序漸進。你不用擔心它講的是廢話,因為它總能解決你在前面提出的問題(如果認真去學,問題總是很多的),編程中遇到的問題,總能在某一個知識點找到解決方法,理解其中的為什么。
-
當你遇到問題時,幾乎只能用英語去表述問題並去 google ,當然結果幾乎都來自於 Stack Overflow,有時還需要 The Standard ML Basis Library 。
-
並不是說有用的只有我記下的那么多(而且還加工了不少),考慮到閱讀材料中的一些內容已經足夠精簡了。如果對這門課真有興趣,不妨去上上看。
筆記
1. All values are expressions. Not all expressions are values.
2. SML 中沒有賦值語句(assignment statement),它只有 變量綁定(variable binding) 以及 函數綁定(function binding)。
Bindings are immutable. Given val x = 8+9; we produce a dynamic environment where x maps to 17. In this environment, x will always map to 17; there is no “assignment statement” in ML for changing what x maps to. That is very useful if you are using x. You can have another binding later, say val x = 19; , but that just creates a different environment where the later binding for x shadows the earlier one. This distinction will be extremely important when we define functions that use variables.
an ML program is a sequence of bindings.Each binding adds to the static environment (for type-checking subsequent bindings) and to the dynamic environment (for evaluating subsequent bindings).
3. 它的講述模式是這樣的:先給出SML內置的常見的數據結構或語法,我們可以很容易去使用。再通過介紹核心的東西(數據結構的本質)以及特定的語法,以至於我們可以自己去定義這種數據結構(list 、option 都可以通過 datatype 定義出),重要的是核心的語法是極其少的。
定義 list :
datatype my_int_list = Empty
| Cons of int * my_int_list;
fun append_mylist (xs, ys) =
case xs of
Empty => ys
| Cons(x, xs') => Cons(x, append_mylist(xs', ys));
val lst = Cons(1, Cons(2, Empty));
append_mylist(append_mylist(lst, Cons(1, Cons(3, Empty))), lst);
在這里 Empty 是 my_int_list
類型的值,而 Cons 是 int * my_int_list -> my_int_list
類型的函數。
4. 為什么函數名和參數間會有一個空格呢?看到閱讀材料中的代碼,心中不由產生疑惑,尤其是當一個函數只有一個參數時,我們可以直接省略其中的括號,例如:
fun f x = x + 1;
f(123);
f 123;
定義一個函數,接受參數 x 返回 x+1,原本只以為是內建語法,一個語法糖而已,等到后面看到了模式匹配,才發現並不是這樣。為什么不需要括號呢?因為參數本來就是一個整體,即只有一個參數。
fun f (x, y, z) = x + y + z;
看起來很正常的一個函數,其實它只是在模式匹配下的一種簡化,其實它應該是這樣的:
fun f(t: int * int * int) =
case t of (x, y, z) => x + y + z;
也就是說它其實是接受一個 tuple 類型的參數,將對應位置上的值分別綁定到 x, y, z 三個變量上。
也就是說,在 SML 中一個函數只會接受一個參數(one-argument function)。難怪前面說過我們要拋卻以前對於 C、Java 的觀念來學習這門課。
更准確地說,函數一定會接受一個參數,那么問題來了,無參數函數該怎么定義呢?其實和其它語言類似:
fun f () = "HI";
這時候必須加上一個括號了,而這個括號其實是一個 unit 類型的值。那么對應的,在調用函數的時候,應該這樣:
f ();
括號是一定不能省略的,否則它輸出的是這個函數的類型:val f = fn : unit -> string
。
其實,datatype unit = ()
在 SML 中是預定義的,用的也是前面提到的 datatype 。
5. 匿名函數
如果我們想要將一個函數作為參數去傳遞,但並不想把它直接綁定到當前環境中,此時就可以使用匿名函數了。
首先我們可以使用簡單的語法來自己模擬匿名函數:
fun f1 (addone, x) =
addone x;
f1 (let fun f x = x + 1 in f end, 11);
當然,我們也可以使用關鍵字去定義匿名函數。
f1 (fn x => x + 1, 11)
比起簡單的關鍵詞定義,那么我們模擬的這種匿名函數是不是就沒有任何意義了呢?其實不然,匿名函數最顯著的特點是什么呢,匿名,那么問題來了,沒有名字,怎么遞歸呢。想支持遞歸,還得有個名字。
fun f1 (f, x) =
f x;
f1 (let fun f x =
case x of
0 => 1
| q => q * f (q - 1)
in f end,
7)
6. Environments and Closures
首先,不得不提 lexical scope。
我們知道,在 SML 中函數就是值,而函數這個值由兩部分組成,組成這個函數的代碼(即函數本身),以及我們創建這個函數時的環境(environment),當我調用這個函數時,實際上使用的是創建這個函數時的環境(environment),而那個環境里則可以進行一系列的計算,並產生結果。而這個環境和調用這個函數時的環境則是隔絕的,所以我們可以說一個函數構成了 function closure。
val x = 1
fun f y =
let
val x = y + 1
in
fn z => x + y + z
end
val x = 3
val g = f 4
val t = 5
val z = g 6
z 的值為 15 。
val x = 1
fun f y =
fn z => x + y + z
val x = 3
val g = f 4
val t = 5
val z = g 6 (* 15 *)
z 的值為 11 。
7. Currying and Partial Application
簡單來說,就是一個本身具有多個參數的函數(其實仍是一個參數,即一個 tuple),我們把它變成一個只接受第一個參數,並返回一個接受第二個參數的函數,更多的參數類似。例如:
fun fold1 (f, acc, xs) =
case xs of
[] => acc
| x::xs' => fold1 (f, f(acc, x), xs')
fun fold2 f = fn acc => fn xs =>
case xs of
[] => acc
| x::xs' => fold2 f (f(acc, x)) xs'
val sum = fn (x, y) => x + y
val l = [1, 2, 3, 4]
val res1 = fold1 (sum, 0, l)
val res2 = (((fold2 sum) 0) l)
val res3 = fold2 sum 0 l
與 Scheme 相比,SML 看起來清爽很多(如果你以前寫過那種結尾帶着成噸的右括號的 Scheme 程序的話)。事實上,SML的括號是可選擇的,即使不加括號,它也有默認的結合規則,關鍵字也起到了分割代碼的作用。
由於 SML 中存在默認的結合規則,計算 res3 的表達式和計算 res2 的表達式完全相同。
而一個 curried function 的定義也可以簡寫成:
fun fold3 f acc xs =
case xs of
[] => acc
| x::xs' => fold2 f (f(acc, x)) xs'
它們的類型:
val fold1 = fn : ('a * 'b -> 'a) * 'a * 'b list -> 'a
val fold2 = fn : ('a * 'b -> 'a) -> 'a -> 'b list -> 'a
val fold3 = fn : ('a * 'b -> 'a) -> 'a -> 'b list -> 'a
柯里化(Currying) 使得我們可以更加靈活的使用函數。
fun fold f acc xs =
case xs of
[] => acc
| x::xs' => fold2 f (f(acc, x)) xs'
val is_even = fn (x, y) => x + (if y mod 2 = 0 then 1 else 0)
val count_even = fold is_even 0
val res1 = count_even [1, 2, 3, 4]
val res2 = count_even [2, 3, 4, 10, 11]