第(06)個程式的主要功能,在介紹多重條件下,如何產生邏輯比較後分支?題目的問題是:設計氣候報告,輸入溫度大於華氏85度時輸出為太熱,低於55度時輸出為太冷,居中時輸出為剛剛好。
:
\ Example 6. Weather Reporting
: weather ( nFahrenheit -- )
DUP 85 >
IF ." Too hot!" DROP
ELSE 55 <
IF ." Too cold."
ELSE ." About right."
THEN
THEN
;
\s
You can type the following instructions and get some responses from the computer:
90 weather Too hot!
70 weather About right.
32 weather Too cold.
能執行邏輯判斷後條件分支,是程式語言三大支柱之一,另兩個,則是能逐步執行,與能循環執行。我設計 ABC Forth 時也得驗證出具有這三項功能,才能用來解決所有的數學計算問題。今天,除了學學這個 Forth 程式外,我改用 ABC Forth 寫出這個程式,實現同樣的基本要求,然後解說 ABC Forth 與傳統 Forth 程式語法間的不同。
:
Forth 已發展成語言為結構化的規格,早期的 Forth 使用者,還常強調不改系統照樣能突破結構化的技術,後來就不強調了。現代的 Forth 還能辦得到,但也很少人談論與應用這種性能。我的 ABC FORTH 執行起來能亂跳,就是利用 Forth 的這種天性使然。請不要以為不結構化不好,它有一定要存在的絕對必要性,最基本的組合語言是一切程式語言的根本,它,就得能夠亂跳。我的 ABC Forth 中,能亂跳的指令是 GOTO,但實際上 IF ----- THEN 中的 THEN 也能亂跳。
:
我先把程式列在這裡再解釋:
:
integer t
: T? ( n -- )
[[ t ]] !
BASIC
10 if t > 85 then 50
20 if t < 55 then 70
30 print " About right. "
40 goto 100
50 print " Too hot! "
60 goto 100
70 print " Too cold. "
100 end ;
比較兩套程式,功能完全沒有問題,請自行copy這一段程式,附貼到第(06)個程式的後面,載入程式測試後便知。
:
上列程式中,[[ t ]]能夠取得整數變數 t 的位址。ABC Forth 程式在尚未執行到 BASIC 這個指令前,可以完全只使用傳統 Forth 的語法寫程式,但在 basic 至 end 間,就得使用 BASIC 程式語言的語法寫程式。若在其中仍想要用 Forth 語法寫程式,我自創了一個叫作 RUN 的 BASIC 語法專用指令,可以辦到這項要求,它的設計內容,就是叫用 INTERPRET 來完成,這裡還不急著介紹其用法,以後再說。
:
Forth 邏輯判斷後分支的 IF ----- ELSE ----- THEN 整組指令結構,是在 IF 的前面進行邏輯運算,留下結果供 IF 使用而分支。 BASIC 邏輯判斷後分支的設計,在我的 ABC Forth 中,只有一種用法,就是只能在 then 的前面進行邏輯運算,算完,供 then 使用而分支。而且,分支的要件是只能分支到後續指定的標號列去,沒有其他任何指令,所以,我前述說到的 then 就能亂跳。但,回跳,就得用標號的負數,這樣使用的例子,以後提到時再說。
:
這只是一個小範例,卻展示了使用 BASIC 語法設計的程式,能夠完全被融入 Forth 系統的特異功能。上列設計好的 T? 指令,完全能像任何其他純用傳統 Forth 語法設計出來的程式那樣,被後來的程式直接叫用。所以,我把堆疊規格也標示在 T? 後面:( n — ),跟原設計出來的 weather 指令規格完全一樣。
:
由於 ABC FORTH 是編譯(compile)式而非執譯(interpret)式的語言,所以執行速度不會變慢。而且,我的設計內容不採用傳統 BASIC 系統的設計方式,程式被執行時,系統裡面沒有將程式一列一列拿來分析的剖析器(parser),而是根據 Charles H. Moore 設計的編譯器(compiler)直接編譯出來的,所以執行速度跟 Forth 一樣快。您若有興趣,可以使用 Win32Forth 中的 see 指令直接看看 T? 被編譯後的內容,執行 see T?,就會有結果,但很難看懂,編譯完畢後,就沒有標號了。所以,程式也不會耗費太多的記憶體,其本質根本就還是個 Forth。
:
多重邏輯判斷後分支的問題,在任何程式語言中,都是個麻煩問題,分支要求一多,就會把流程搞混,程式就很難看懂。 為此,FORTH 後來就發展出 CASE ----- ENDCASE 的語法,來解決這方面的問題。1994 年以後,這一組指令已被列為標準規格。雖已為標準,但尚力有不逮,碰到條件是區域而不是單純一個數值能表達的狀況時,仍得更改設計,所以也不是很好用,反而是我設計的這種分支方法,能涵蓋所有的層面。這一個簡單範例只有三個分支,還不需要使用 CASE 來處理,它只展示兩層的分支程式該怎麼寫?
:
我們討論的題目內容不太複雜時,我會趁機討論一些程式語言的哲理,題目複雜時,就少談哲理。
:
第(07)個程式介紹以程式印出九九乘法表。
:
程式語言只有三大支柱,第三個,是能夠循環執行。這個程式就是個使用循環指令 DO ----- LOOP 的例題。
:
\ Example 7. Multiplication table
: onerows ( row -- )
CR
DUP 3 .R 3 SPACES
13 1
DO I OVER *
4 .R
LOOP
DROP ;
: Table ( -- )
CR CR 6 SPACES
13 1
DO I 4 .R LOOP ( display column numbers )
13 1
DO I onerows
LOOP
;
\s
usage:
table
DO ----- LOOP 是一種定量式的循環,也就是說它的起始與結束之指標數值是固定的。例如:從 1 做到 10 這樣的環路固定量,環路指標在環路中就會從 1 自動變化到 10 為止。相關於這種環路的指令還有好幾個,這個例題內沒有用到,我就不提。實際上,我不太喜歡使用這種用法設計程式,因為它存在著不少缺陷。例如: Forth 把指標處理的不太合乎人們的直覺,當您想印出 1 到 10 時,該給的上下限是 11 與 1,要倒過來給,上限還得多加了 1。又如:我在設計許多應用程式時,很不習慣於上下限指標相同時環路還會至少仍做一次。如此一來,我在設計函數時,常碰到有上下限一樣程式便不得執行之要求也得被包括的情形,結果就出差錯了。必須改用非標準的 ?DO 指令來取代 DO ,才能解決問題。反對將 ?DO 列為標準的人,不懂得這些問題。還有其他問題,以後遇到時再談。這些不好用的問題,我若改成以 BASIC 格式設計程式,就能解決問題。
:
這個程式,可以改寫成以 ABC FORTH 的格式重新設計,我曾將它寫進貼文,貼出於我的個人網頁,就在2018年2月15日 星期四刊出的『 Calculation 』一文中,請參考:
:
https://forthfortnight.blogspot.com/2018/02/
Calculation
Ching-Tang Tseng
Hamilton, New Zealand
16 February 2018
ilikeforth@gmail.com
http://forthfortnight.blogspot.com
文中程式如下:
3 integers I J K
: table3 ( -- )
BASIC
10 RUN CR 2 SPACES
20 FOR I = 1 TO 12
30 RUN I 4 U.R
40 NEXT I
50 RUN CR
60 FOR J = 1 TO 12
70 RUN CR 2 SPACES
80 FOR I = 1 TO 12
90 LET K = I * J
100 RUN K 4 U.R
110 NEXT I
120 NEXT J
130 END
;
在 ABC Forth 中,我設計的環路格式,用法如下:
FOR I = 起始值 TO 終止值 STEP 間隔值
——————
( 在此區域內使用指標 I )
——————
NEXT I
另外,設計此系統時,我把正在使用中的兩個上下限指標值,設計成暫時存放在普通的數據堆疊上,不像傳統的 Forth 系統是放在回返堆疊上。因此,使用者就很容易自行破壞環路,可以自由地跳出環路去到想要跳去的地方。跳出去時,執行一次 2DROP 就可以亂 GOTO 了。這個功能很有用處,在大量查找質數時,為讓程式能節省執行次數亦即節省時間,就能運用這種程式設計的技巧。它,真的是很有用處。
:
從上列改寫出來的程式中,就能注意到傳統 BASIC 式程式語言印出文字時有個嚴重缺點,就是每印一次就必須跳一列。這樣的性能,不能滿足用來印出一個九九乘法表的要求。我創作的一個叫做 RUN 的指令,能解決這種問題,緊隨 RUN 之後,可以直接使用執行完畢後不會影響堆疊狀態之所有的 Forth 指令。使用方式,就如上述。
:
這個 RUN 指令在 Charles H. Moore 提出的原始設計中沒有,是我獨創的。它可以將 BASIC 程式語法大大的強化,等同於讓使用者在 BASIC 的使用環境中,能夠叫用所有的 Forth 程式。這個程式光用 BASIC 中的 PRINT 指令,不能辦到印出九九乘法表所要求的結果,只能用我創作的 RUN 指令,配合 Forth 系統原本的性質來完成。Forth 處裡輸出與輸入的能力很強,才容易做控制工作,其 I/O 性能是廣義性的,不只是文、數字在螢幕上的 I/O,也包括電腦在實體訊號方面的 I/O。這個程式的展示,只是個小範例。
:
外國人用12x12乘法表,我們中國人才用9x9乘法表。
:
第(08)個程式規模較大,是個完整的設計,主題是:根據年、月,或單只根據年,印出月曆。
所有的電腦都得處理日期與時間,電腦內的日期與時間資料影響很多事情,嚴重的,可以令電腦跑不起來。例如:以前,硬體組態設定 CMOS 所需的電力不足時,這個資料就會歸原成出廠時的原始時間,就不能上網,我曾經吃過這種虧,只好每次開機時,人工調整出正確的日期與時間才上網。
我試了三套不同作業系統的電腦,都能印出月曆,在顯示的動態時間訊息上,用滑鼠點一下就行,操作摘要如下:
W10 --> click right down corner
Ubuntu --> click right upper corner
Mac --> click right upper corner
在命令提示字元全黑視窗(command console)中,都可以下這個命令得到時間,寫程式需要時,可用來叫用: $ date
顯然,這麼重要的事情,電腦內必須有軟體來處理,還必須被設計成可以被公眾方便叫用的程式。因此,很多人寫這種相關程式來當作範例教材。Win32Forth 系統中 Demos 資料夾內,就有一個叫作 CalendarDemo 的範例,它採用物件導向程式(OO,Object-Oriented programming)的設計方式,印出一個半年曆。您可以自己打開該檔案來看看,我保證您看不懂程式寫法與程式中的叫用方法。OO 程式方式已經被淘汰了,以前與以後的慣常 Forth ,都不會具有這種功能,無法傳承,所以,您也別浪費時間去研究,我會用,但不想浪費時間介紹。
這個範例程式的執行原理,是根據一個簡化後的儒略日(julian day)計算程式,只處理1950年以後的日期,算出某個月份第一天的儒略日數值。根據這個數值,就能決定這一天必定是星期幾?再配合另外的指令,可以算出該年是否為閏年?該月有幾天?就能印出月曆。想想看,如果不給你程式,只給你這三個參數,您是否也能手寫出月曆?若是,這個程式也就是根據這個道理印出月曆的。關於儒略日這個術語,後面還有精確的算法範例程式,會再解釋,這裡就不討論,暫時只要知道,它能用來算出某年某月某日必定是星期幾,就夠了。
應用時,程式最後,提示了使用方法。
細研時,程式後面的說明很健全,每個指令執行前後堆疊上數據的變化,都有詳細的敘述。
學習時,請看看如何寫程式?學會怎樣才能把程式寫得清楚明白?日後便於追查、校正、再利用。程式中也有 CASE ----- ENDCASE 多重分支的標準用法,可以學習。
想摸通輸出月曆格式印出來的設計方法,就得實際去操作,才能看到結果。
:
\ Example 8. Calendars
VARIABLE JULIAN ( Julian date of 1st of a year, from Jan. 1, 1950)
VARIABLE LEAP ( 1 for a leap year, 0 otherwise. )
1461 CONSTANT 4YEARS ( number of days in 4 years )
: YEAR ( YEAR --, compute Julian date and leap year )
1949 - 4YEARS 4 */MOD ( days since 1/1/1949 )
365 - JULIAN ! ( 0 for 1/1/1950 )
3 = ( modulus 3 for a leap year )
IF 1 LEAP ! ( leap year )
ELSE 0 LEAP ! ( normal year )
THEN ;
: FIRST ( MONTH -- 1ST, 1st of a month from Jan. 1 )
DUP 1 =
IF DROP 0 ( 0 for Jan. 1 )
ELSE DUP 2 =
IF DROP 31 ( 31 for Feb. 1 )
ELSE DUP 3 =
IF DROP 59 LEAP @ + ( 59/60 for Mar. 1 )
ELSE 4 - 30624 1000 */
90 + LEAP @ + ( Apr. 1 to Dec. 1 )
THEN
THEN
THEN
;
: DAY ( DD MM YYYY -- JULIAN-DAY )
YEAR ( Compute JULIAN and LEAP)
FIRST + 1- ( add DD to 1st of the month )
JULIAN @ + ( add to Jan. 1 of the year )
;
: STARS 0 DO 42 EMIT LOOP ; ( form the boarder )
: header ( n -- ) ( print title bar )
cr cr 26 stars space
case 1 of ." January " endof
2 of ." February " endof
3 of ." March " endof
4 of ." April " endof
5 of ." May " endof
6 of ." June " endof
7 of ." July " endof
8 of ." August " endof
9 of ." September" endof
10 of ." October " endof
11 of ." November " endof
12 of ." December " endof
DROP
endcase
space 27 stars cr cr
." SUN MON TUE WED THU FRI SAT"
cr cr ( print weekdays )
;
: BLANKS ( MONTH -- ) ( skip days not in this month )
FIRST JULIAN @ + ( Julian date of 1st of month )
7 MOD 8 * SPACES ; ( skip columns if not Sunday )
: .DAYS ( MONTH -- ) ( print days in a month )
DUP FIRST ( days of 1st this month )
SWAP 1 + FIRST ( days of 1st next month )
OVER - 0 ( loop to print the days )
DO I OVER +
JULIAN @ + 7 MOD ( which day in the week? )
0= IF CR THEN ( start a new line if Sunday )
I 1 + 8 U.R ( print day in 8 column field )
LOOP
DROP ; ( discard 1st day in the month )
: MONTH ( N -- ) ( print a month calendar )
DUP
HEADER DUP BLANKS ( print header )
.DAYS ; ( print days )
: CALENDAR ( YEAR --- ) ( print year calendar )
YEAR ( compute JULIAN and LEAP )
13 1 DO I MONTH LOOP ( print 12 month calendars )
CR CR 64 STARS ; ( print last boarder )
\s
Usage:
yyyy YEAR mm MONTH
or
yyyy CALENDAR
第(09)個程式,是一個可以用來精算出任何年代之儒略日的程式。
前一個範例程式,用來計算儒略日的設計方法,只適用於 1900 年以後的日期,算起來就能比較快。
這個程式,則適用於人類信史以後,所有的年代與日期,計算量較多。
安排百例的原則,本應以簡短精要為主,前一程式稍長,但因事涉一個完整的典型應用,所以,我仍採用。這樣安排百例,不是隨興之所致而編排,其來有自。展示了前例,若還有牽扯,就接續產生下一個相關範例。前十個範例,仍然是以傳統的 Forth 程式格式為主,我才能借助於這樣的前引,導入我們的正軌,後面才引介 ABC FORTH 。
我接觸『儒略日』術語的日期,是早在民國 62 年(1973)我唸大三時。那時,我們只能寫 FORTRAN 程式,一學期只允許跑 11 次 mainframe 大電腦。終端機管理員,是學校剛畢業的應數系學長,很兇,要是沒有跟他們搞好關係,想要獲得一份電腦印出來、帶有蒙娜麗莎微笑之年曆,那就別想。於是,我就花時間了解曆法,研究月曆的產生方式,結果,從天文曆書上找到了答案,知道何謂儒略日?知道憑它印出月曆的規矩。天文學與曆法學對我們在校的學習成績沒有幫助,不會有助於畢業後按成績的分發,我只是有興趣把這個電腦內一定要用到的基本程式搞清楚。我從 1973 年的 Astronomical Almanac 學到了這麼一句話:『儒略日數加 1,以 7 除之,餘數即星期順序。』這本書,現在還在。我不想浪費篇幅介紹何謂儒略日?有興趣,請自己查一下 Google,就可以得到答案。我在今天的貼文中,告訴您相關於 Forth 資料的來源,反而比較重要。
網上的 Forth 科學程式庫中,第 22 個貢獻,就是計算儒略日的程式。網頁是:
Julian day in FSL source:
The Forth Scientific Library Project
https://www.taygeta.com/fsl/sciforth.html
||
||
\||/
\/
22. Conversion between calendar date and Julian day
https://www.taygeta.com/fsl/scilib.html
從源程式資料中,您可以看出,簡化算法與精確算法,兩種都有。簡化只需利用精確算法,得到 1900.01.01 的日數,以其重新做為計算基準,就能改採較為簡單的固定參數來簡化計算。簡化相關參數的 ABC FORTH 程式寫法,放在第(16)個程式內當範例。
與日期、時間相關的程式,在電腦系統中都很重要,除了搞月曆的日期,還有搞時間顯示的程式,我都能自己設計,搞程式設計的人就該學會這門技術。APPPLE II 的時代,我曾為台灣中壢工業區的力固磅秤公司,以 6522 晶片產生標準時基,設計一整天都能夠產生的精確時間,並將其設計成可被叫用的程式,他們要用它來管制卡車進出貨時的記錄,需要精確時間。我辦到了,賺到了一台謝崧梅贈送的撞擊式印表機。告訴大家這個故事,說明了程式技術是有價值的東西,請好好珍惜。如果有一天,我們也用 Forth 設計出作業系統,這些程式技術都得用上。
:
\ Example 9. The true Julian date
: JULIAN-DATE ( DD MM YYYY -- d, Julian date as a double integer )
>R ( save YYYY on return stack)
DUP 9 + 12 / ( 0 for Jan/Feb, 1 for others)
R@ + 7 * 4 / NEGATE ( take 1.75 days out for each year)
( 365.25=367-1.75 )
OVER 9 + 12 / NEGATE
R@ +
100 / 1 + 3 * 4 / - ( leap days generated by centuries)
SWAP 275 9 */ ( days in year before this month)
+ + ( add DD, days in year and misc)
S>D 1.721029 D+ ( add Julian date of Jan 1, 0 AD)
367 R> UM* D+ ( add days of prior years )
;
: whatday ( dd mm yyyy -- )
julian-date d>s 1+ 7 mod . ;
\s
Usage:
dd mm yyyy whatday
例:
1 1 2013 whatday 2
第(10)個程式,以整數表示法算出數學函數 sin 及 cos 。
這個程式原本是 16 位元時代的產物,已經不適合現代,但仍有存在於單板微處理器上的價值,所以留作範例。
16 位元的時代,最大單整數只能表示到 65535 , 32 位元則可以有 10 位數, 64 位元可以有 18 位數。
所以, 8 位元的時代,微處理器上,不適合搞浮點運算,16 位元則勉強尚可,4 位有效數字仍有用途。
32 位元以後,才是比較可以正式使用浮點運算的恰當環境,已經可以有 10 位有效數字, 64 位元就更不用說了。
我熟悉浮點系統中基本函數的設計方法,學過,也試用過無數種算法,知道各種算法之優劣,包括只用硬體數學運算處理器來獲得函數的方法,也非常熟悉。所以,我設計的 ABC FORTH 系統中都具有浮點運算的全套功能。今天可以使用這些浮點性能分析問題,但只用來討論此第(10)個程式的設計內容。
最有效率的 sin 與 cos 函數值產生方法,是使用泰勒(Taylor)級數展開式的算法。我先把式子列在這裡:
sin(x) = x - x^3/3! + x^5/5! - x^7/7! + ….
cos(x) = 1 - x^2/2! + x^4/4! - x^6/6! + ….
算法學上,強調效率,上列表示式必須改成計算次數少、能夠算得快、不會造成溢位的多括弧表示式。若只取四項,可以寫成:
sin(x) = x(1-x^2/6(1-6x^2/120(1-120x^2/5040(1-5040x^2/362880))))
cos(x) = (1-x^2/2(1-2x^2/24(1-24x^2/720(1-720x^/40320))))
上列表示式,就是用來設計這兩個函數的根據式子。
在整數系統中沒有小數點,函數值只好以放大位數的方式表示,在 16 位元中的最大整數為 65535 ,為了能夠健全顯示所有的數值,只能安排少掉一位數的方式表示,結果,數字只適合放大一萬(myriad)倍,所以,程式中有一個宣告產生的常數叫做 10K 。放大一萬倍後,函數值只剩四位數有效值。而輸入的格式,被設計成採用 0 到 90 度數的方式,例如:角度為 37 度,就使用『 37 sin . 』的方式獲得 6017 的結果,表示所得的函數值是 0.6017 。您可以用硬體數學計算處理器測試答案,應該是:
37.0e0 FPI F* 180.0e0 F/ FSIN F. .601815
我的 ABC FORTH 提供了一個更方便的計算器(calculator)式的使用法,可以直接執行出下列結果,不妨自行試試看:
REXP sin ( 37 * FPI / 180 )
RANS .601815
換句話說,這個程式,使用起來沒有問題,誤差只有萬分之一。
程式中,執行 (sin) 時,用到幾個常數,它們的來源,是上列式子中各個係數的倒數,例如:72、42、20、6 就是上列 sin(x) 表示式中常數部分之倒數:
72=362880/5040
42=5040/120
20=120/6
6=6/1
同理,可以類推出執行 (cos) 時所需用到的幾個常數。
只有 4 位有效位數的函數值有什麼用?還很有用。
最簡單的舉例,就是直接用它設計螢幕上的繪圖,螢幕的解析度一般只有 1024 點左右,萬分之一的精確度很夠用了,繪製動態展示圖時,用這種純粹單整數的函數算法計算時,可以很省時間,產品就可以具有快速的動感。
以前,我們也用這種方法設計自動焊接機器的定位驅動,由於算得很快,驅動效果非凡,燒焊弧線,一點問題也沒有。
我搞工程,熟知工程界把千分之一的精確度叫做一條,算是很精準。以前,德國人賣的工作母機配上光學尺,生產時,才能有這樣的精確度。一般工廠中使用的儀錶,精確度平均都只有 5% ,好錶的精確度也只有 3% 。我任公職退休前管理過的原子爐中,只有一個超級精確的重水液位顯示儀器,可以有 1% 的精確度。
這樣的解釋,告訴大家,萬分之一,很準了。
:
\ Example 10. Sines and Cosines
31415 CONSTANT PI
10000 CONSTANT 10K ( scaling constant )
VARIABLE XS ( square of scaled angle )
: KN ( n1 n2 -- n3, n3=10000-n1*x*x/n2 where x is the angle )
XS @ SWAP / ( x*x/n2 )
NEGATE 10K */ ( -n1*x*x/n2 )
10K + ( 10000-n1*x*x/n2 )
;
: (SIN) ( x -- sine*10K, x in radian*10K )
DUP DUP 10K */ ( x*x scaled by 10K )
XS ! ( save it in XS )
10K 72 KN ( last term )
42 KN 20 KN 6 KN ( terms 3, 2, and 1 )
10K */ ( times x )
;
: (COS) ( x -- cosine*10K, x in radian*10K )
DUP 10K */ XS ! ( compute and save x*x )
10K 56 KN 30 KN 12 KN 2 KN ( serial expansion )
;
: SIN ( degree -- sine*10K )
PI 180 */ ( convert to radian )
(SIN) ( compute sine )
;
: COS ( degree -- cosine*10K )
PI 180 */
(COS)
;
\s
Usage:
To test the routines, type:
90 SIN . ( 9999 )
45 SIN . ( 7070 )
30 SIN . ( 5000 )
0 SIN . ( 0 )
90 COS . ( 0 )
45 COS . ( 7071 )
0 COS . ( 10000 )
沒有留言:
張貼留言