編程語(yǔ)言的一些基礎(chǔ)概念(一):靜態(tài)函數(shù)式編程

世界上最好的編程語(yǔ)言是什么?

這就好像問(wèn) 世界上最好的車(chē)是什么車(chē)?F1 比賽的,日常家用的和跑山路的最好的車(chē)顯然是不一樣的。同理,不同的編程語(yǔ)言也有他們最適合的使用場(chǎng)景,程序員們通常都會(huì)個(gè)幾種語(yǔ)言,因?yàn)楣ぷ餍枰赡芤獙W(xué)新的語(yǔ)言。不同編程語(yǔ)言之間是不是完全不一樣呢?他們之間有沒(méi)有什么共同點(diǎn)是不同語(yǔ)言間類似的呢?有沒(méi)有一些最基本的概念?

最近在看了 Coursera 上的 Programming Languages, Part A,對(duì)于這個(gè)問(wèn)題做了部分解答,這里做個(gè)階段性的總結(jié)。

這是門(mén)什么樣的課?

Programming Languages 這個(gè)課程是華盛頓大學(xué)的公開(kāi)課,評(píng)分很高,包括在知乎上,有很多人推薦。

課程在 Coursera 上一共有三部分,分別用不同的編程語(yǔ)言來(lái)做基礎(chǔ)概念的說(shuō)明和對(duì)比,既有函數(shù)式編程,也有面向?qū)ο缶幊?,Part A 用的是 Standard ML (SML),Part B 是 Racket,Part C 是 Ruby,分別代表了不同類型的編程語(yǔ)言,為什么是這三門(mén)語(yǔ)言,作者在課程里有詳細(xì)的解釋。

不同類型的編程語(yǔ)言

這個(gè)課的重點(diǎn)不是在這幾門(mén)語(yǔ)言本身,而是一些編程語(yǔ)言的概念,和使得這些語(yǔ)言變得優(yōu)雅的語(yǔ)言特性。

函數(shù)式編程 和 SML 的一些語(yǔ)言特性

整個(gè)課程的系列就介紹到這,Part A 的重點(diǎn)是函數(shù)式編程以及 SML 的一些語(yǔ)言特性。包括了函數(shù)式編程的一些概念 數(shù)據(jù)不可變 (Immutable Data),頭等函數(shù) (First-Class Function),高階函數(shù) (Higher-Order Function) 等,以及 SML 本身的一些特性 類型推論 (Type Inference),柯里化和偏函數(shù)應(yīng)用 (Currying and Partial Application),模塊 (Module) 等。

類型推論(Type Inference)

SML 是靜態(tài)類型語(yǔ)言( Statistically Typed),在編譯時(shí)進(jìn)行類型檢查,同時(shí)它又是隱式類型 (Implicit Typed),可以省略類型聲明,在編譯時(shí)會(huì)自動(dòng)做類型的推導(dǎo),來(lái)判斷類型是不是正確的。

val x = 42
fun f(y,z,w) = if y then z+x else 0

函數(shù) f 的參數(shù)省略了聲明類型,編譯器從 x = 42z+x 可以推導(dǎo)出 x 和 z 的類型是 int,從 if y 可以推導(dǎo)出 y 的類型是 bool。這個(gè)特性能讓代碼簡(jiǎn)潔不少。

List 的用法和非函數(shù)式編程很不一樣

在 SML 里,List 的最主要功能有兩個(gè) hd 代表 head,取數(shù)組的第一個(gè)元素,tl 表示 tail,數(shù)組的第一個(gè)之外的全部元素,因?yàn)橛羞@樣的特性,使得函數(shù)式編程的數(shù)組遞歸變得很順其自然。

fun sum_list (xs : int list) =
    if null xs
    then 0
    else hd(xs) + sum_list(tl xs)

多態(tài)的數(shù)據(jù)類型(Polymorphic Datatypes)

SML 的數(shù)據(jù)類型有 int, bool, real, string 等具體的,函數(shù)接受的數(shù)據(jù)類型除了這些之外,也可以是多態(tài)的。

fun length xs =
    if null xs
    then 0
    else 1 + length(tl xs)

在這個(gè)例子里, xs 是一個(gè) list,可以是一個(gè) int list, string list 或者 bool list 等,所以 SML 表示 xs 的類型是 'a list 'a 表示數(shù)據(jù)類型的多態(tài),也是能夠通過(guò) Type Inference 推導(dǎo)出來(lái)的。

數(shù)據(jù)不可變(Immutable Data)

在 SML 里,沒(méi)有賦值的概念,而是由一系列的 綁定(bindings)組成的。比如說(shuō)

(* 這里不是賦值,而是建立了一個(gè)新的 x 的 binding,
   在當(dāng)前的環(huán)境下,x 是不能更改的,永遠(yuǎn)是等于 1 的 *)
val x = 1

(* 這里并不是對(duì) x 的值進(jìn)行修改,而是新建了一個(gè) x 的 binding,
     前面的 x 被這個(gè) shadow 了,所以當(dāng)前的環(huán)境下,x 永遠(yuǎn)是 3 *)
val x = 3 

數(shù)據(jù)不可變是函數(shù)式編程最主要的特性之一,優(yōu)點(diǎn)在于去除了不同代碼之間的依賴性,不需要像 Java 這類語(yǔ)言一樣去考慮List 傳址,一個(gè)地方代碼對(duì)值的更改,不會(huì)導(dǎo)致另外一個(gè)部分的代碼出錯(cuò)。

fun append (xs : int list, ys : int list) =
    if null xs
    then ys
    else (hd xs) :: append(tl xs, ys)
    
val xs = [1, 2]
val ys = [3, 4]
val z = append(xs, ys)

上面這個(gè)例子的作用是 append 兩個(gè) list,:: 這個(gè)符號(hào)是把一個(gè)值插入到一個(gè) list 的第一位。因?yàn)閿?shù)據(jù)不可變的性質(zhì),上面代碼中直接返回的是一個(gè)新的 list, ys 的值不會(huì)因?yàn)槭潜?append 的那個(gè) list 而改變。

反過(guò)來(lái)看一個(gè) Java 的數(shù)據(jù)可變,帶來(lái)了安全隱患的例子,是以前 Java Library 里真實(shí)存在的一個(gè) Bug 的簡(jiǎn)化版。


class ProtectedResource {
   private Resource theResource = ...;
   private String[] allowedUsers = ...;
   public String[] getAllowedUsers() {
      return allowedUsers;
   }
   public String currentUser() { ... }
   public void useTheResource() {
      for(int i=0; i < allowedUsers.length; i++) {
         if(currentUser().equals(allowedUsers[i])) {
             ... // access allowed: use it 
             return; 
         }
            }
        throw new IllegalAccessException();
   }
}

雖然 allowedUsers 是 private 的,但是 getAllowedUsers 是 public 并且返回的是 allowedUsers,導(dǎo)致了使用這個(gè)庫(kù)的人可以直接修改 allowedUsers 給自己增加權(quán)限。當(dāng)然修復(fù)這個(gè)問(wèn)題也很簡(jiǎn)單,在 getAllowedUsers 里返回 allowedUsers 的一個(gè) copy 就行了。這里想要說(shuō)明的是在數(shù)據(jù)不可變的編程語(yǔ)言里,不用去擔(dān)心數(shù)據(jù)可變帶來(lái)的一些負(fù)面影響,當(dāng)然 Java 的傳址也有它的好處。

頭等函數(shù)和高階函數(shù)(First-Class Function and High-Order Function)

頭等函數(shù)是能作為一個(gè)參數(shù)傳遞到另一個(gè)函數(shù)的函數(shù)。高階函數(shù) 是可以接受函數(shù)作為參數(shù)也可以返回函數(shù)作為結(jié)果的函數(shù)。直接來(lái)看個(gè)例子

fun f g = 
        let val x = 3 
        in
        g 2
    end
val x = 4
fun h y = x + y
val z = f h

這里 h是個(gè)頭等函數(shù),被當(dāng)成一個(gè)參數(shù)傳入高階函數(shù) f 中使用。頭等函數(shù)和高階函數(shù)也是函數(shù)式編程中很重要的概念。除了函數(shù)式編程之外,在現(xiàn)在熱門(mén)的一些編程語(yǔ)言里,有一些是支持這個(gè)特性的,比如 Python 和 Javascript。

匿名函數(shù)(Anonymous Functions)

在要把一個(gè)函數(shù)作為參數(shù)傳遞到另一個(gè)函數(shù)時(shí),又不想在環(huán)境中去綁定這個(gè)函數(shù),可以使用匿名函數(shù)。

fun f (g, x) =
  g x

(* 1. 可以定義函數(shù) *)
fun increment x = x + 1
f (increment, 11)

(* 2. 匿名函數(shù) *)
f (fn x => x + 1, 11)

這個(gè)功能在 Javascript 和 Golang 里也是支持的。

// Javascript 例子
(function(x, y){
    alert(x + y);  
})(2, 3);

詞法域(Lexical Scope)

跟頭等函數(shù)息息相關(guān)的是詞法域,函數(shù)里變量的值是創(chuàng)建函數(shù)時(shí)的值,而不是調(diào)用函數(shù)時(shí)的值,創(chuàng)建環(huán)境和調(diào)用環(huán)境時(shí)隔絕的,可以說(shuō)一個(gè)函數(shù)構(gòu)成了 function closure。還是用上面那個(gè)例子

(* 例子1 *)
fun f g = 
        let val x = 3 
        in
        g 2
    end
val x = 4
fun h y = x + y
val z = f h

val z = f h ,函數(shù) f 被調(diào)用,f 里 x 的值應(yīng)該是 4 還是 3 呢?按照上面的定義,函數(shù) h 被創(chuàng)建時(shí),x 的值是4,所以 g 2 調(diào)用 h 時(shí),應(yīng)該是 4 + 2 而不是用重新定義的 val x = 3。再看另一個(gè)例子

(* 例子2 *)
val x = 1
fun f y =
    let val x = y + 1
    in
        fn z => x + y + z (* 這是一個(gè)匿名函數(shù) *)
    end
val g = f 4
val x = 3
val y = 5
val z = g 6

val g = f 4 得到 g 是一個(gè)函數(shù) fn z => x + y + z,運(yùn)行 val z = g 6 時(shí),x + y + z 的 x 應(yīng)該是什么值呢?x 的值在運(yùn)行 val g = f 4 時(shí)就已經(jīng)綁定了,val x = y + 1 所以 x 的值是 5,后面定義的 val x = 3 不會(huì)更改函數(shù) g 里 x 的值。

根據(jù)這兩個(gè)例子,詞法域和**動(dòng)態(tài)作用域 **(Dynamic Scope) 比有什么優(yōu)點(diǎn)呢?

  1. 可以隨時(shí)修改變量的值,不會(huì)影響到函數(shù)的調(diào)用。
  2. 在例子1中,val x = 3 沒(méi)任何作用,直接刪除沒(méi)什么問(wèn)題。但是如果是在動(dòng)態(tài)作用域里,可能別的代碼用到了這個(gè)值,刪除了會(huì)影響到別的程序正常運(yùn)行 ,可以說(shuō)詞法域使減少了代碼之間的依賴。
  3. 在例子2中,將 val x = 3 改成 val x = "hi",因?yàn)橛性~法域,代碼不會(huì)有任何問(wèn)題,但是如果是動(dòng)態(tài)作用域的話,即使編譯能過(guò),但是會(huì)將 string 和 int 相加,產(chǎn)生額外的一些問(wèn)題。

尾遞歸(Tail Recursion)

簡(jiǎn)單理解,在遞歸的過(guò)程中,每一步不需要再做別的運(yùn)算,直接將結(jié)果傳遞給下一層遞歸。

fun fact1 n = if n=0 then 1 else n*fact(n-1)

(* 尾遞歸 *)
fun fact2 n =
 let fun aux(n,acc) =
     if n=0
     then acc
     else aux(n-1,acc*n)
 in
     aux(n,1)
 end

上面這兩個(gè)函數(shù)都是計(jì)算 n 的階乘。fact1 在每次遞歸返回時(shí),都要進(jìn)行一次乘法運(yùn)算,而 fact2 則是通過(guò)一個(gè) Accumulator 將值向下一層傳遞。對(duì)于第一種普遍的遞歸方式,每一層遞歸都緩存在棧里,所以有一個(gè)棧的堆疊,但是對(duì)于尾遞歸來(lái)說(shuō),SML 進(jìn)行了優(yōu)化,不會(huì)有多個(gè)棧的堆疊,返回的時(shí)候直接返回最后的值。不單單 SML 對(duì)尾遞歸有優(yōu)化,像 Scala 這些函數(shù)式編程語(yǔ)言應(yīng)該都有。

fact1
fact2

柯里化和偏函數(shù)應(yīng)用 (Currying and Partial Application)

把接受多個(gè)參數(shù)的函數(shù),變換成接受一個(gè)單一參數(shù)的函數(shù),并且返回接受剩下參數(shù)的新函數(shù),這個(gè)方法叫做 柯里化??聪旅娴睦樱?/p>

(* 1. 接受多個(gè)參數(shù) Tuple *)
fun sort (x,y,z) = z >= y andalso y >= x
sort (1, 3, 2)

(* 2. 柯里化 *)
val sort = fn x => fn y => fn z => z >= y andalso y >= x
((sort 1) 3) 2) 

(* 3. SML 柯里化的簡(jiǎn)潔寫(xiě)法 *)
val sort x y z = z >= y andalso y >= x
sort 1 3 2

不論是函數(shù)的定義,還是調(diào)用時(shí),都會(huì)比較簡(jiǎn)潔,除了這個(gè)優(yōu)點(diǎn)外,柯里化使得函數(shù)的調(diào)用更加靈活。

fun fold f = fn acc => fn xs =>
  case xs of
    []     => acc
  | x::xs’ => fold f (f(acc,x)) xs’

val xs = [1, 2, 3]
val sum1 = fold (fn (x,y) => x+y) 0 xs
val sum2 = fold (fn (x,y) => x+y) 0

在上面這個(gè)例子中,我們先綁定了一個(gè)函數(shù) fold,作用個(gè) map reduce 的 reduce 一樣,把一個(gè) list,按照參數(shù)函數(shù) f 的方式,從 acc 開(kāi)始計(jì)算。sum1 直接用 fold 這個(gè)函數(shù)計(jì)算 xs 的總和。因?yàn)?fold 是柯里化的,所以可以像 sum2 那樣只提供了兩個(gè)參數(shù),返回一個(gè)輸入是第三個(gè)參數(shù)的函數(shù),這就是 partial application 。sum2 是一個(gè)函數(shù),參數(shù)是一個(gè) list,結(jié)果是計(jì)算它的總和。

組合函數(shù) (Combining Functions)

一個(gè)函數(shù)要用到另一個(gè)函數(shù)的返回結(jié)果作為參數(shù)時(shí),組合函數(shù)可以讓代碼更簡(jiǎn)潔,其實(shí)也是一個(gè)管道的概念。

(* 常規(guī)寫(xiě)法 *)
fun sqrt_of_abs i = Math.sqrt(Real.fromInt(abs(i)))

(* 組合函數(shù)寫(xiě)法 *)
fun sqrt_of_abs i = (Math.sqrt o Real.fromInt o abs) i

(* 去除 Unnecessary Function Wrapping *)
val sqrt_of_abs = Math.sqrt o Real.fromInt o abs

互遞歸(Mutual Recursion)

這是一個(gè)之前沒(méi)有注意到的概念。兩個(gè)函數(shù)互相調(diào)用,函數(shù) f 里調(diào)用 g,函數(shù) g 里調(diào)用 f,在運(yùn)行到函數(shù) f 時(shí),g 還未被綁定,在 SML 里,通過(guò) and 來(lái)實(shí)現(xiàn)這種互遞歸。

fun odd(x) = 
    if x=0 then false   else even (x-1)
and even(x) = 
    if x=0 then true else odd (x-1)

Python 和 Java 也是支持這種互遞歸的,具體怎么實(shí)現(xiàn)的還不了解。

def even(x):
    if x == 0:
        return True
    else:
        return odd(x-1)

def odd(x):
    if x == 0:
        return False
    else:
        return even(x-1)

模塊化(Modules)

模塊化是要進(jìn)行大規(guī)模開(kāi)發(fā)必不可少的,不同的語(yǔ)言在模塊化上的實(shí)現(xiàn)方式都不太一樣,SML 是通過(guò) structure 來(lái)定義模塊,用 signature 來(lái)定義模塊的類型,相當(dāng)于是模塊的接口。代碼比較復(fù)雜,不具體展開(kāi)了。

編程語(yǔ)言的組成

Part A 除了介紹函數(shù)式編程和 SML 的語(yǔ)言特性外,還穿插了些編程語(yǔ)言的基本概念。

每個(gè)變成語(yǔ)言由下面這些組成:

  • 語(yǔ)法(Syntax): 語(yǔ)言是怎么寫(xiě)的?怎么定義變量,函數(shù)等?
  • 語(yǔ)意(Semantics): 語(yǔ)言特性是什么樣的?比如說(shuō)表達(dá)式是怎么被執(zhí)行的?
  • 風(fēng)格(Idioms): 語(yǔ)言的編程風(fēng)格,怎么寫(xiě)是這個(gè)語(yǔ)言的最佳實(shí)踐?
  • 庫(kù)(Libraries): 語(yǔ)言有哪些已經(jīng)可以用的庫(kù),可以大大提高效率?
  • 工具(Tools): 語(yǔ)言相關(guān)的開(kāi)發(fā)工具,比如說(shuō)編譯器,debuggers 等

在學(xué)習(xí)一門(mén)新語(yǔ)言的時(shí)候,最基本的是要了解他們的語(yǔ)法,但是最重要的是要知道語(yǔ)意和代碼風(fēng)格。根據(jù)不同使用場(chǎng)景選擇不同的語(yǔ)言,也可以按照這些部分去考慮。語(yǔ)法簡(jiǎn)單還是復(fù)雜,庫(kù)和工具是否齊全會(huì)影響開(kāi)發(fā)效率,語(yǔ)意和風(fēng)格是什么樣的?有一些會(huì)影響運(yùn)行效率。

沒(méi)有最好的編程語(yǔ)言,只有更好的程序員

雖然車(chē)有很多中不同的類型,車(chē)的架構(gòu),組成部分等都是差不多的,對(duì)于機(jī)械工程師來(lái)說(shuō),最主要是知道車(chē)的原理,而不是不同型號(hào)的車(chē)具體是什么樣的。對(duì)于程序員來(lái)說(shuō)也是一樣的,我們?nèi)粘J煜ひ婚T(mén)或者幾門(mén)編程語(yǔ)言,但了解編程語(yǔ)言的基本原理,對(duì)于用好現(xiàn)在的語(yǔ)言,還有以后學(xué)習(xí)一門(mén)新的語(yǔ)言都是很有幫助的。

總的來(lái)說(shuō),這個(gè)課對(duì)于有編程基礎(chǔ)的朋友來(lái)說(shuō)難度不大,課程難度也循序漸進(jìn),作業(yè)設(shè)計(jì)的也挺不錯(cuò)的。雖然學(xué)完這個(gè)課沒(méi)有辦法馬上應(yīng)用到什么地方,短時(shí)間內(nèi)可能不會(huì)對(duì)你現(xiàn)在的工作有什么影響,但是可以幫你打好基礎(chǔ)。想學(xué)習(xí)函數(shù)式編程,想要增加對(duì)編程語(yǔ)言理解的朋友們,還是很推薦的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容