Scheme嚐鮮
最近幾天晚上在看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")))
)
)
這裡用到了事先定義好的常量One2Twenty和Tens,將它們與字串列表(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 呼叫了遞迴的letterCount ,letterCount 則呼叫了遞迴的transNum2English,而transNum2English又呼叫了遞迴的getElement。但是執行過程卻很快,感覺也就1秒左右。
ps:這裡,我的Scheme編譯環境是Racket,用起來還不錯,可以從Racket網站下載。
相關文章
- React Suspense 嚐鮮React
- React Loops 嚐鮮ReactOOP
- 鴻蒙系統嚐鮮鴻蒙
- Windows 10 週年版嚐鮮Windows
- Go 1.17 泛型嚐鮮Go泛型
- Vue.js 2.6嚐鮮Vue.js
- Node.js 嚐鮮筆記Node.js筆記
- Flutter新版本 Web App 嚐鮮FlutterWebAPP
- Oracle 19c 安裝嚐鮮Oracle
- TiDB 4.0 新特性嚐鮮指南TiDB
- ent orm筆記1---快速嚐鮮ORM筆記
- Webpack5.0 新特性嚐鮮實戰 ??Web
- 【轉】Kinect嚐鮮(1)——第一個程式
- HTML5中dialog元素嚐鮮HTML
- Spring Cloud Gateway 閘道器嚐鮮SpringCloudGateway
- 利用Conda嚐鮮Python 3.10Python
- MySQL·8.0新特性·Newdatadictionary嚐鮮篇MySql
- 嚐鮮雲端地圖服務AzureLocationBasedServicePreview地圖View
- 【Flutter桌面篇】Flutter&Windows應用嚐鮮FlutterWindows
- AngularJS嚐鮮——快遞運費計算AngularJS
- CSS3嚐鮮(一):CSS多列布局CSSS3
- Vue嚐鮮快速、零配置的打包工具—parcel~Vue
- TiDB at 豐巢:嚐鮮分散式資料庫TiDB分散式資料庫
- 嚐鮮少程式碼高效能的Svelte框架框架
- Vue嚐鮮快速、零配置的打包工具---parcel~Vue
- 市民花百元裝Win7“嚐鮮”Win7
- Linux Kernel 2.6 核心執行緒嚐鮮(轉)Linux執行緒
- 客戶端效能測試利器PerfDog嚐鮮體驗客戶端
- Vue3 外掛開發詳解嚐鮮版Vue
- Java協程程式設計之Loom專案嚐鮮Java程式設計OOM
- 中國風?古典系?AI中文繪圖創作嚐鮮!⛵AI繪圖
- Flutter嚐鮮:跨平臺移動應用開發Flutter
- 【嚐鮮】flutter3桌面版微信EXEFlutter
- 嚐鮮:Gradle構建SpringBoot(2.3.1最新版)GradleSpring Boot
- 寫註釋就能自動出程式碼?copilot 嚐鮮
- Hadoop 2.0.0-alpha嚐鮮安裝和hello world薦Hadoop
- ARM釋出Cortex-A73全新旗艦CPU 海思嚐鮮!
- Vue3全家桶 + Vite + TS + TSX嚐鮮,先人一步!VueVite