艾均 蘇湛



摘要:函數式編程語言及函數特性在工業界逐漸流行,函數式編程語言教學具有重要的理論與現實意義。以提高教學質量為目的,通過仔細分析默認不可變、高階函數、模式匹配、數據與函數解耦等編程語言特征,采用討論對比與實踐方法,研究函數式編程語言教學方法,對學生編程思維進行訓練,并對未來編程技術發展趨勢進行分析。采用實例編程教學與不同語言對比相結合的方法,使《函數式編程語言》教學質量得到有效提升。
關鍵詞:F#;函數式編程;編程實踐;教學特點;教學內容組織
DOI:10.11907/ejdk.191325開放科學(資源服務)標識碼(OSID):
中圖分類號:G433文獻標識碼:A 文章編號:1672-7800(2019)010-0201-03
0引言
函數式編程范式,解耦了數據和處理數據的函數,將數據在不同處理函數之間的流動過程展現給用戶,使用戶能夠對自己的業務邏輯始終保持專注,避免了命令式編程不斷向計算機描述如何完成某項工作的瑣碎步驟,也避免了面向對象抽象過程中數據和方法的耦合以及類繼承的復雜性。函數式編程與命令式編程范式相比,具有數學上的優雅性。因為計算機編程過程處理的對象是數據,函數才是處理不同有組織數據的核心。在初步掌握數據結構后,學生可通過函數式編程語言了解利用計算機解決計算問題的思想,進而將這種思想應用于計算機編程實踐。
F#是微軟.NET開發平臺的一門編程語言,由微軟倫敦研究院研發,目前是全平臺的開源編程語言,技術演進完全由社區驅動,其最鮮明的特點是對函數式編程范式(FP,FunctionalProgramming)的引入,同時F#對面向對象(OOP)編程的支持也同樣出色。使用F#語言,開發人員可以自由選擇函數式編程或面向對象編程實現目標。此外,F#還可與NET平臺上的C#、VB等編程語言緊密結合,通過相互調用完成歷史代碼復用,實現更大規模的項目。
函數式編程思想與傳統的C語言、較新的Python、Go語言不同,具有一定新穎性,帶來解決問題的新思路,尤其是對于并行與異步編程,相比Java等編程語言,更加利于學生理解問題的關鍵和核心。近年函數式編程語言在工業界流行,在大學開設相關課程是教改關注的熱門話題。這種范式的編程語言與大眾熟知的面向對象、命令式編程語言不同,為編程人員提供了新的問題解決思路與思維工具。盡管這種編程語言在工業界和學術圈流行,但尚未在國內高校編程教學中得到重視。
本文從函數式編程的新視角出發,研究了函數式編程特點,用不同的方式解決傳統軟件開發的諸多問題。啟發學生思維,為解決老問題提供新思路。
1默認不可變
函數式編程語言強調量默認的不可變性,如C語言中,入門時一定會告訴學生需要定義一個整形變量sum,初始值為0,當對1-100的整形數求和時,可以不斷將1-100中的數字i加到sum中,改變sum變量的值為舊的sum加上新的i,這樣可以求得最終的和值。在這個過程中,編程者向計算機仔細描述了實現一個數列求和功能的函數。
而在函數式編程中,因為量默認是不可變的,就不能進行累計的求和累加行為,需要換一種思路。從代碼1可以看到,1~4行構造了一個遞歸函數,這個遞歸函數有兩個輸入:第一個輸入data是一個要求和的list,第二個輸入是用來求和的sum。接下來的match with關鍵字是現代編程語言,諸如Scala、Swift等編程語言,都有類似的語法特征(如用Switch實現這一功能)。可以看到“0”豎線部分代表一種需要被匹配的情況,如果匹配成功,則執行箭頭指向的語句塊。匹配規則是如果data的list由一個頭元素head和若干尾元素tail(這里的tail是一個去掉data頭元素的list)組成,那么函數會將頭元素加到sum上,得到新的sum,并對tail尾進行上述操作,不同的是輸入給getSum函數的sun值已經變成在原來的sum值上加了head值。在第5行中,當data的list為空時,可以看到getSum函數會將sum輸入的結果直接返回給用戶。在代碼1第6行中,可以看到一個求1到100的和的例子,data輸入對應[1…100],代表一個從1到100,步長為1的list,sum輸入一個整形0作為和的初始值。
代碼1:基于函數式編程語言的不可變、模式匹配特性進行求和。
從這段代碼可以看出,編程人員在實現過程中沒有用到任何可變變量,一切值在所有行里都是不可變的,這保證了當代碼在多核或多線程中運行時沒有任何副作用,是純粹(pure)的。
量默認不可變優勢非常明顯,任何一段代碼拿到一個量都可以放心使用這個量,不必擔心代碼在其它線程或內核中改變這個值,這在機制上克服了副作用,避免了加鎖等一系列繁瑣操作。
2高階函數與聲明式編程
分析例證1:求一個list的和,可以給一個初始值,將list中每個頭元素不斷遞歸與初始值求和,并作為新的初始值送人下一次遞歸,直到list為空,即可求得整個list的和值,這其實就是MAP/REDUCE計算模型中的REDUCE過程。
代碼2:描述做什么(WHAT),而不是訴說怎么做(WHAT)。
分析例證2:通過一個計算實例向學生展現函數編程語言的優雅與簡潔。當存在一個list,從1到100,要求對其中的每一個元素做某種運算f生成一組新的元素,再對其中符合某種條件t的元素進行保留,最后對剩余元素求和。
通過提問方式討論傳統編程語言解決這一問題的方法。通常發現需要對list元素進行遍歷,計算遍歷元素i在廠函數作用下的結果f(i),然后判斷f(i)是否符合條件t。如符合,將其值加入一個累計變量sum中作為最終的和;如不符合t則不進行任何操作。而在F#代碼2中可以看出,第1行構造目標數據,第2行對數據中每一個元素進行操作(將每個元素平方,然后加1),第3行對新生成數據進行過濾,保留其中可以被2整除的元素,第4行對結果進行求和。第2行和第3行中,直接將一個需要進行的操作和判斷的條件放在map函數及filter函數后面,對需要進行操作或過濾的數據進行操作或過濾,這樣可以接受一個作為輸入的函數,叫做高階函數。學生在學習到這一特性時,普遍會覺得函數式編程的語法更容易理解,同時看起來很優雅。
有了高階函數,函數式編程賦予編程者組合拼接各種功能函數的能力,從而通過若干小的功能就可構造出更大更多功能。而高階函數直接避免了指針(或代理)的使用,可以將函數很輕松地作為像量一樣的東西傳遞,這種將函數和量保持在同等地位的特性也是函數式編程獨有的。
3并行計算與異步計算
隨著單純提高CPU頻率滿足摩爾定律的終結,中央處理器的核心數量越來越多,并行并發計算在各種編程語言中都是學習難點。而F#的語法設計在學習并行與異步計算課程中異常簡單。示例代碼如代碼3、代碼4所示。
代碼3:數組同步并行計算代碼示例。
代碼3與代碼2的運算完全一致,不同的是代碼2的計算由單個CPU核心完成,代碼3則會使用CPU所有的邏輯核心和實體核心。仔細比較兩段代碼,可發現它們高度類似,只是list換成了array及額外增加了一個parallel。之所以將list換成array,是因為list對模式匹配的支持更豐富,但array通過下屬parallel模塊支持同步并行計算。如示例所示,當對一個集合中每個元素作某種變換(用一個函數處理元素)時,可以直接使用并行計算,這樣F#編程中的同步并行計算就可輕易達成。講授到這里時,學生會感嘆函數式編程的易用性。但還需補充講解list和array數據類型的特點和區別,避免學生在學習中混淆。
代碼4:數組異步并行計算代碼示例。
同步并行問題在于當計算量較大時,CPU因所有核心都在執行目標操作,無暇顧及其它系統請求,會給用戶造成卡頓的感覺。當希望CPU進行計算的同時還可響應必要的系統請求,就需要異步并行計算。還是同樣的計算任務,代碼4展示了在F#中使用async關鍵字構造一個異步計算并完成的過程。
第3到第5行的代碼將原本要進行的計算包裹在一個結構async{return something}中,代表一個要進行的異步計算,something是這個異步要返回的結果。構造完這個異步計算,第2到第6行就將1~100的100個元素映射成了100個異步計算,第7行的Async.Parallel用fork-join模式,將100個異步計算合并成一個異步計算,在第8行代碼的執行命令下,返回這個異步計算結果(注意這時結果是一個數據Array)。
講授該知識點時,可請學生在課堂上討論各種編程語言是如何實現異步編程的。在向大家介紹F#的異步實現方式后,通過對比,學生會發現F#不需要構造線程、設計Runnable類對象、聲明回調函數等瑣碎細節就可直接構造一個異步運算,并可執行和獲得結果。
4結語
治學當知行合一(王守仁),學習計算機相關知識尤其需要學生動手實踐。當學生面對并行、異步、高階函數、默認不可變時,這些概念對他們來說是陌生的。通過代碼示例、課堂討論、動手實踐,學生能掌握函數式編程的核心概念和解決問題的一般思路。聲明式編程將編程人員的焦點從告訴計算機如何做轉移到告訴計算機做什么上。計算機編程已經到了一個需要進一步向前演化的時間節點,Scala、F#等函數式編程語言和函數式語法已在工業界逐步流行,這要求學校具有前瞻性,為學生儲備相關知識和基礎技能。
通過學習《F#函數式編程》課程,學生可理解面向對象編程之外的另一種編程思想,為其將來工作中解決生產實踐問題提供新的思維工具。通過不斷探索不同知識點的教學方法,學生更容易理解一些全新概念,豐富知識儲備和思維方式,對培養新時代的計算機專業人才具有重要意義,計算機編程教學質量也將得到進一步提升。