Scala函數語言程式設計原理 第二課 程式設計的本質(Elements of programing)
從這周開始,我將開始學習Scala程式設計,我們將由淺入深的學習這門新的語言和函數語言程式設計正規化。這堂課的大部分內容對你來講將會非常的簡單易懂,因為這都是你熟悉的東西。但是,也有一些東西是對以後學習打下根基的東西,特別像是我們稱之為“代替模型”的求值前的模型等知識,將會對以後的課程非常的重要,希望大家多多注意。好的,讓我們開始吧:
每一門非凡的計算機語言都有以下幾點:
1、表達最簡單資料單元的原始表示式
2、提供各種結合表示式的方式
3、提供抽象這些表示式的途徑,可以讓這些表示式抽象成名字然後被呼叫等等。
運用函數語言程式設計有點像用計算器,一種很好的學習方式就是用語言本身提供的互動式的shell,你可以利用它書寫一些表示式,它會返回給你所寫表示式的值,Scala提供的是叫REPL的小工具。(Read-Eval-Print-Loop)想要啟動Scala的REPL工具只需在命令列下敲出“scala”就可以:
然後,你就可以寫Scala了。如果出現以上的東西,表明你的Scala安裝正確了,但是在我們的課程中,我們不會講Scala基礎安裝的東西,這不屬於這堂課的範疇,我們只會 講SBT(Scala 包管理工具,類似於Java中的Maven)等東西的安裝。在SBT中,你照樣可以得到類似於這個REPL互動命令列,但是你輸入的東西將是“sbt console”而不是“Scala”。
在Scala的REPL下,你可以輸入“34+65”;或者定義一個函式,都會返回相應的值:
好的,下面讓我們來一起看到了資料的求值,資料的求值遵循的是代數的基本法則,對於一個非原始表示式的求值遵循以下原則:
1、用最左邊的操作符,
2、對運算元求值(從左往右)
3、把操作符運用於運算元
那麼,對於一個name的求值是怎樣的呢?對於一個name的求值,就是對於定義這個name右邊的內容的求值。 求值過程一旦遇到返回值,則求值過程就會終止。在此時刻,返回的值就是一個數字(如上圖)。之後,我們還會考慮其他型別的返回值。
下面就是一個數學表示式的求值:
(2*PI)*radius
首先我們會求出PI的值
(2*3.14159)*radius
然後我們會求出括號中的值
6.28318*radius
然後求出radius的值
6.28318*10
最後得到結果,並返回結果
62.8318
定義表示式(definitions )還可以包含引數,我們可以利用“def square“ 語句來定義一個square函式,然後傳一個double型別的引數x,等號右邊的x都是引數傳來的x,於是咱們可以算得square(2)等於2乘以2等於4,以此類推,得到square(5+4)等於81.0,square(square(4))等於256.0。我們還可以定義square的和,如下所示。
我們可以發現,在最後一行,函式的引數後面跟了一個數值型別,這個數值型別在引數的冒號之後,在Scala中,函式的引數符合這種規定。你也可以給一個此函式的返回值,在Scala中是寫在引數的後面,如下所示:
def pow(x:Double, y: Int): Double = ...
Scala的原始資料型別都來自於Java,但是是大寫的:
Int 32位整型數字
Double 64位浮點型數字
Boolean 布林型的值 (true或者是false)
那麼一個函式程式是如何求值的呢?
程式的引數化的函式的求值和運算元的求值方式類似:
1、從左到右依次求出所有函式引數的值
2、把函式程式由函式右邊的內容所代替,函式的結果都是等號右邊得到的結果
3、與此同時,把形參全部換成真實的引數
下面,我們通過一個例子來看一下:
sumOfSquares(3, 2+2)
==> sumOfSquare(3, 4)
==> square(3) + square(4)
==> 3 * 3 + square(4)
==> 9 + square(4)
==> 9 + 16
==> 25
這種格式的表示式求值被稱為"代替模型"或者“替換模型”。
這個想法成為一種模型的原因在於:表示式可以等價於一個值。(所有表示式都是值)
這可以被所有表示式所使用,只要他們沒有副作用。什麼是副作用?一個副作用肯定呼叫了是表示式,例如:c是一個可變變數,“c++”的意思就是每次執行“c++”c就會加一。“C++”這個表示式就是有副作用的,因為沒用一次,就會產生新的值,一個表示式無論什麼時候呼叫,都返回同一個值,這樣的表示式就可以被稱之為沒有副作用。
這種“代替模型”在lamda表示式中得到了很好的實現,拉姆達表示式是函數語言程式設計打下了根基。
總結:一旦我們有了“代替模型”,另外一個問題就出現了,是否每一個表示式都能轉化成單獨的一個值(在有限的很少的步驟之內)?
事實上,答案是:不行。下面有一個反例:
有一個簡單的函式名字叫loop,返回值是Int型,指向的卻是loop
def loop:Int = loop
這會出現什麼情況呢?根據我們的取值流程,我們必須求出函式的值:把函式右邊的求值過程代替左邊的函式名。但是,右邊的步驟卻又是loop。
loop ===> loop
所以,我們把表示式沒有簡化到一個值,而是又轉到了自己身上,並且這種轉化將會一直進行下去。。。
loop ===> ...
另外一種方式去更形象化的理解這種迴圈步驟,從自身名字開始相當於畫了一個圓圈,然後又從頭開始畫圈。。。
直譯器會把函式的引數在重新執行函式程式的時候減少成為一個值,這不是唯一的解決方案。換句話說,我們可以提供一個不減少引數的方式,例如:
sumOfSquares(3, 2+2)
==> square(3) + square(2 + 2)
==> 3*3 + square(2+2)
==> 9 + square(2+2)
==> 9 + (2+2) * (2+2)
==> 9 + 4 * 4
==> 25
現在你發現了兩種方式去對相同的表示式求值,第一種方式叫做 all by value, 第二種方式叫做 call by name。
兩種方式都會被簡化成了一個相同的數值,只要滿足以下條件:
1、被簡化的表示式包含純函式,
2、兩種求值方式都會終止而不是無線迴圈。
call by value 有一個特點就是會一次性的求出所有引數的數值。
call by name 有一個特點是如果說函式的引數沒有被用到,它就不會求出它的值,引數只有在被用到的時候,才會去求值。
下面有一個問題:有如下函式:
def test (x: Int, y: Int) = x * x
對於一下表達式的使用,指出"call by name"和"call by value"哪種求值方式更快?(或者說有更少的求值步驟?)
test(2,3)
test(3+4, 8)
test(7, 2*4)
test(3+4, 2*4)
讓我們來一起看看這個問題,讓我們從第一個開始:
test(2,3)
call by value:==> 2*2 ==> 4
call by name:==> 2*2 ==>4
很顯然,這種方式call by name和call by value得到的步驟都是以上的方式,所以兩種求值方式所用的步驟是一樣的。
來看第二種:
test(3+4, 8)
call by value: ==> test(7, 8) ==> 7*7 ==>49
call by name: ==>(3+4) * (3+4)==> 7*(3+4) ==>7*7 ==> 49
很顯然,call by value方式取勝
來看第三種:
test(7, 2*4)
call by value : ==> test(7,8) ==> 7*7 ==>49
call by name: ==> 7*7 ==>49
call by name取勝。我們來看最後一個:
test(3+4, 2*4)
call by value:==> test(7, 2*4) ==> test(7,8) ==> 7*7 ==>49
call by name:==>(3+4) * (3+4) ==> 7*(3+4) ==>7*7 ==> 49
又是一樣的
視訊地址:https://www.coursera.org/learn/progfun1/lecture/vzbJj/lecture-1-2-elements-of-programming