Scheme嚐鮮

bnuwangly發表於2016-03-26

最近幾天晚上在看Robert Sebesta的《Concepts of Programming Languages》(第7版影印版,高等教育出版社)一書的第15章《Functional Programming Languages》。這一章提到了LISP,Scheme,Common LISP,ML,Haskell等函式式程式設計語言,但主要的篇幅用在了介紹Scheme上。這可能是因為Scheme最簡單,在不大的篇幅內就能夠說明清楚,並達到讓學習者上手編寫程式的目的。

看完了這一章的內容,不禁想動手試一試,於是選擇了以前做過的Euler Program的第17題。以前寫過一篇用Excel解這題的文章,見這裡。這裡只把題目再次拿過來。

Problem 17:Number letter counts

If the numbers 1 to 5 are written out in words: one, two, three, four, five, then there are 3 + 3 + 5 + 4 + 4 = 19 letters used in total.

If all the numbers from 1 to 1000 (one thousand) inclusive were written out in words, how many letters would be used?

NOTE: Do not count spaces or hyphens. For example, 342 (three hundred and forty-two) contains 23 letters and 115 (one hundred and fifteen) contains 20 letters. The use of "and" when writing out numbers is in compliance with British usage.

在Scheme裡,一切皆是函式。只不過有一些函式是由Scheme語言提供的,剩下的則需要我們自己去寫。就拿加法(+)來說吧,在命令式語言(如我們通常學的C、C++、Java)中我們認為+是運算子,但其實我們也可以認為它是函式。既然是函式,就有定義域(domain set)有值域(value set)有對映規則(mapping rules)。這裡我們假設定義域是整數,則值域也是整數,從定義域中取1個以上的數,在值域就有相應的整數與之對應,如取出3和4,與之對應的值就是7。在Scheme中,相應的語句則是:

(+ 3 4)

更一般的形式是:

(function_name [para1] [para2] ……)

加[ ]表示引數可有可無,函式的引數可以1個或者多個,也可以是0個,如(+)返回0。要定義自己的函式,語法是:

(define (function_name [para1]  [para2]) (function_body))

比如,我們可以自己定義一個接受兩個引數的加法,

 (define (myAdd num1 num2) (+ num1 num2))

然後就可以這樣呼叫了:

(myAdd 1 2)

現在回到Euler Program的第17題,計算1到1000對應的英文單詞共用了多少個字母。具體到這個問題,我們需要實現下面三個函式:

(define (totalLetterCount num) (todo))
(define (letterCount str) (todo))
(define (transNum2English num) (todo))

函式totalLetterCount有一個整數引數num,計算從1到num對應的英文單詞共用了多少個字母,是從整數到整數的對映。

函式letterCount則有一個字串引數str,計算該字串中有多少個字母,是從字串到整數的對映。

函式transNum2English也有一個整數引數num,將整數num轉變成英文單詞,是從整數到字串的對映。

假設函式letterCount和函式transNum2English已經實現,則我們不難寫出totalLetterCount的實現:

(define (totalLetterCount num)
 (if (= num 1)
   (letterCount (transNum2English 1))
   (+ (letterCount (transNum2English num)) (totalLetterCount (- num 1)))
 )
)

如果num等於1,則返回1對應的英文單詞字母的個數,否則的話返回num對應的單詞的個數加上1到num-1所對應的英文單詞字母的個數。在Scheme中,沒有迴圈結構,遞迴佔有很重要的位置,我們可以通過遞迴去實現迴圈。這裡用到了if結構,其語法形式是

(if predicate then_statement else_statement)

predicate是能夠判斷真假的語句,這裡是(= num 1),即num是否等於1,如果為真(Scheme中真為#t),則執行then_statement ,為假(#f)則執行else_statement。 函式letterCount 的實現也不復雜,如果字串str中沒有空格的話,可以直接用Scheme自帶的string-length函式,但這裡涉及到空格,需要我們自己寫一個。下面是letterCount 的實現:

(define (letterCount str)
  (cond
    ((= (string-length str) 0) 0)
    ((= (string-length str) 1) (if (string=? str " ") 0 1))
    (else (+
            (letterCount (substring str 0 1))
            (letterCount (substring str 1 (string-length str)))
          )
    )
  )
)

這裡用到了cond多重選擇的結構,類似於C語言中的switch,其語法是

(cond
  (predicate_1 expression)
  (predicate_2 expression)
  (...)
  (else expression)
)

如果str的長度為0,直接返回0;如果str的長度為1,則判斷是否是空格,如果是返回0,否則返回1; 如果大於1,則將str分成兩部分,第一個字元組成的字串和剩下的字串,分別計算這兩部分包含的字母數,然後再相加就是最後的結果。這裡我們再次用到了遞迴。 最複雜的是函式transNum2English,思路與之前的文章相同,也是根據引數num的取值分不同情況處理。

(define (transNum2English num)
  (cond
    ((< num 21) (getElement One2Twenty num))
    ((< num 100)
      (if (= (remainder num 10) 0)
          (getElement Tens (quotient num 10))
          (string-append (getElement Tens (quotient num 10)) " "
                         (getElement One2Twenty (remainder num 10))
          )
       )
    )
    ((= num 1000) (string-append "one" " thousand"))
    ((= (remainder num 100) 0) (string-append
                                 (getElement One2Twenty (quotient num 100))
                                 " hundred"
                                )
    )
    ((< num 1000) (string-append
                   (getElement One2Twenty (quotient num 100))
                   " hundred and "
                   (transNum2English (remainder num 100))
                   )
    )
    (else (car '("num should not be greater than 1000")))
  )
 )

這裡用到了事先定義好的常量One2TwentyTens,將它們與字串列表(list)繫結。

(define One2Twenty '("one" "two" "three" "four" "five"
                     "six" "seven" "eight" "nine" "ten"
                     "eleven" "twelve" "thirteen" "fourteen" "fifteen"
                     "sixteen" "seventeen" "eighteen" "nineteen" "twenty"))

(define Tens '("ten" "twenty" "thirty" "forty" "fifty"
               "sixty" "seventy" "eighty" "ninety"))

另外,還用到了從字串列表中根據相應下標取字串的函式getElement,其功能相當於C語言中的[ ]運算子。

(define (getElement array index)
  (cond
    ((= index 1) (car array))
    (else (getElement (cdr array) (- index 1)))
  )
)

該函式有兩個引數,array和index,分別表示字串列表和下標。car和cdr 則是Scheme自帶的函式,分別取列表中的第一個元素和去掉第一個元素後所剩下的列表。比如有一個列表 ( (A) B C D),則car返回 (A) ,而cdr返回 (B C D)。getElement 同樣是遞迴函式。

對Scheme的總體印象是:語法簡單易學;基本不用考慮變數和賦值,只需專心考慮如何實現函式;遞迴在Scheme中很重要,也很強大。

實際上,我們這裡實現的四個函式都是遞迴函式。得到最終答案的程式碼如下,

(totalLetterCount 1000)

執行該語句,最外層的遞迴就已經有1000次,這還不包括totalLetterCount 呼叫了遞迴的letterCountletterCount 則呼叫了遞迴的transNum2English,而transNum2English又呼叫了遞迴的getElement。但是執行過程卻很快,感覺也就1秒左右。

ps:這裡,我的Scheme編譯環境是Racket,用起來還不錯,可以從Racket網站下載。

相關文章