2015讀書進度[漆楚衡]-《Write Yourself a Scheme in 48 Hours》

漆楚衡發表於2015-04-23

2015讀書進度-《Write Yourself a Scheme in 48 Hours》

這周開始了《Write Yourself a Scheme in 48 Hours》,這本書跟《自制程式語言》有些相似,可以跟《自制程式語言》的第一部分對比著讀。

跟《Write》相似的還有《SICP》的第4章,《The Little Schemer》和《The Seasoned Schemer》的最後一章。

這三個都是在Scheme本身下實現Scheme。 說來慚愧,在看《自制》和《Write》的過程中,終於領會到……自己其實沒有看懂這三本書……

主要概念

《Write》是一本Haskell的入門書,走實踐路線,通過實現Scheme告訴你Haskell中的一些概念。

  1. IO Monad
  2. Either (Monad)
  3. Error Monad
  4. Existential Types——展示語言擴充,實現異態列表
  5. Monad Transformer——以前沒搞懂,這裡的也沒看太懂
  6. IORef——通過IO實現可變物件
  7. 檔案處理API
  8. Parsec——haskell的分析器,對比lex/yacc……
  9. ……

我碰到的難點

這本書的內容還是挺簡單的,不過裡面有些地方我還是花了點時間才看懂的,還有些地方目前還不太懂。

  1. 異常:這只是稍微有點麻煩,總體上比這本書的平均難度要高一點,要仔細讀一讀第四章。
  2. 可變數:Haskell本身是沒有可變數的,可變數都遮蔽在IO Monad中了,不過Scheme中沒有那麼嚴格的區分。 對於IO Monad和Either(作為Monad時),始終要記得所有操作都是用bind串聯起來的。也就是函式的最後一個引數總是Monad。 在引入可變數之前,這兩個Monad井水不犯河水,IO是程式的最外層,Either的操作extractValue拆解了,影響範圍不大。 引入可變數之後,二者開始出現交集,用Monad Transformer解決。
  3. Monad Transformer,這個東西之前就不太懂。雖然在這本書中不需要弄懂內部原理就可以繼續讀下去。不過心中還是有疑惑,相關程式碼也只是一知半解(好吧,主要是有藉口看不懂了……)。
  4. 函式進化史:函式處理是書中程式碼重寫次數比較多的地方,需要仔細追蹤演化軌跡
    1. 第三章:實現最基本的算術運算,apply第一次出現,在apply中實現了一個表查詢
    2. 第五章:一大波關係運算降臨,實質改變不大
    3. 第八章:引入變數後,第八章引入使用者定義函式。也引入了跟可變性有關的幾個special form 函式真正成為值,廢除apply中的表查詢。不論原生函式(之前只有原生函式)還是使用者定義函式,都放在Env中,按名索取。
    4. 引入IO Port 因為IO Port與之前的原生函式型別不相容,所以需要另外有張函式表。 注意,因為Env中儲存的是LispVal(所有型別),所以所有函式(原生,IO,自定義)都可以放在Env中。

一些細節

List VS DottedList

在這本書才明白scheme中普通的列表'(a b)和點列表'(a b . c)的區別在於:

  • 前者是(cons 'a (cons 'b '()))
  • 後者是(cons 'a (cons 'b 'c))

即最內部是否為'()

load(scheme)

函式都是在apply中應用的,而函式應用一直都不需要Env。所以apply不知道呼叫函式時的Env

load需要讀取檔案,再在Env(似乎是全域性Env)中掛載檔案裡程式碼的執行結果

書中的解決方法是通融一下,讓load成為special form。不過這樣load就不是函式了,不能放入Env,不能傳來傳去。

我想了一下,也許也可以分解一下:第一部分是一個IO函式,第二部分是Env設定(special form或函式),第二部分先藏在Env中,取一個Scheme非法名稱。 再有一個load對應的函式,首先呼叫IO函式取得掛載表,再用第二個函式掛載。

另外也可以直接暴露全域性Env,說不定還有其它用途(危險)呢。

異常,與Either Monad

雖然書中出現了一個Error Monad,不過好像沒什麼存在感,異常處理主要是對Either的一些操作。

Either a b型別提供了兩個值構造子Left aRight b。一般約定Left a表示a型別錯誤資訊,Right b表示一個正常的b型別的值

Either主要是作為一個Monad來用的,另有兩個實用函式:

  • throwError丟擲異常
  • catchError處理異常

有以下程式碼

fun :: Int -> Either String String
fun x
 | x > 0 = return . show $ x
 | otherwise = throwError "Positive please."

slover :: String -> Either String String
slover str = return str

-- try block
a :: Either String Int
a = Left "No number here"

b :: Either String Int
b = Right (-1)

test1 = catchError (a >>= fun) slover
test2 = catchError (b >>= fun) slover

-- extractValue
v1 = let Right x = test1 in x
v2 = let Right x = test2 in x

在C++中,也許可以寫成下面這樣

string v1, v2;
try {
    a = getInt();
    b = -1;

    v1 = test1(fun(a));
    v2 = test2(fun(b));
} catch(Error_V1 e) {
    v1 = slover(e);
} catch(Error_v2 e) {
    v2 = slover(e);
}

有了熟悉的參照,Either Monad就很好理解了。

兩種異常的目的是一樣的:如果發生異常,則立刻放棄後續執行,跑去catch處理異常。

可以看出來,Either Monad(大部分以Either a b型別結尾的程式碼)就像是在try塊中的程式碼一樣受到“監視”

slover和fun算是例外,不過它們類比於try塊中呼叫的函式。它們也是(必須是)Either a b型別結尾的程式碼,這似乎可以看作受查異常?

不同之處在於catch,Haskell中的異常處理是通過函式指定的,而C++中是通過型別指定的。

三種函式和set!

最終的函式系統分成三部分:

  1. 原生簡單函式
  2. 原生IO函式:需要在IO Monad下操作
  3. 使用者定義函式

除此之外,函式中還會呼叫set行為對Env進行操作。

我當時有些疑問:使用者定義函式會組合這些,特別是IO函式和set行為,會不會產生某種混亂?(參考剛剛想過的load)

思考之後發現,不會。我把Haskell對Haskell函式的要求延伸到Scheme中去了(@_@)。

始終記住,對於現在的語境,Scheme對於Haskell來說只不過是字串。

Scheme中會出現上述幾種函式的交織,但這不關Haskell的事。

這種相互遞迴是Scheme的語義,而在Haskell中,對Scheme分析樹(列表)的執行是在eval(和apply)的遞迴上進行的。

對於一個Scheme普通函式,它內部呼叫set!的操作反映到Haskell中,就是在解釋這個函式時遞迴呼叫eval。

在eval中根據LispVal的值構造子,分派出針對set special form的操作。

原生函式和預定義的special form不會彼此影響,而使用者定義函式在eval時不過是不斷遞迴降解應用eval的過程。

相關文章