匿名函數指函數定義體(即代碼塊)本身,使得函數成為所謂的“一等公民”,函數也可以像變數一樣進行賦值定義、傳遞和使用。本文還介紹了函數式編程的各種技巧,如嵌套、遞歸、高階、閉包等,站在函數式編程的頂峰,讓你感嘆“會當凌絕頂,一覽眾山小”! ...
ShoneSharp語言(S#)的設計和使用介紹
系列(9)— 一等公民“函數“愛炫巧
作者:Shone
聲明:原創文章歡迎轉載,但請註明出處,https://www.cnblogs.com/ShoneSharp。
摘要: 匿名函數指函數定義體(即代碼塊)本身,使得函數成為所謂的“一等公民”,函數也可以像變數一樣進行賦值定義、傳遞和使用。本文還介紹了函數式編程的各種技巧,如嵌套、遞歸、高階、閉包等,站在函數式編程的頂峰,讓你感嘆“會當凌絕頂,一覽眾山小”!
軟體: S#語言編輯解析運行器,運行環境.NET 4.0,綠色軟體,單個EXE直接運行,無副作用。網盤下載鏈接為https://pan.baidu.com/s/1dF4e08p
在所有支持函數式編程的語言中,如LISP, Python, Ruby, JavaScript,函數的地位很高,都被視為“一等公民”。其背後的理論依據就是Lambada演算,業界早已證明其表達能力與傳統過程式程式是等價的。函數式編程有很多技巧(即功能特性),高手能玩出花來。同樣S#語言也支持這些特性,可能語法還更簡潔靈活一點。
本文將全面展示“函數式編程”的各種特性或技巧,註意不是S#獨有,其他函數式語言也都有。其他語言的學習者也可以進來看看,對比一下自己的語言實現方式,可以提高自己的函數式編程領悟力。
一、匿名函數
匿名函數(其他語言也叫lambada式、函數模板、元函數或自定義函數)指函數定義體(即代碼塊)本身,可以當作變數進行傳遞和使用,使得函數成為所謂的“一等公民”。實現方式上其實是通過指向函數入口的指針、地址或引用傳遞,在程式後面調用匿名函數時則把當前程式跳轉到匿名函數體上執行,當然每次執行還要處理參數傳遞和堆棧進出。
S#匿名函數的表達方式很多種,其傳遞的語法解析樹入口節點的引用。
1.1 func專用公式
func(形參序列 : 結果公式)
func專用基本公式建立形參變數堆棧,並對結果公式進行解析,但不求值,最終輸出語法樹入口節點的引用。其中每個形參定義一個變數名稱,有多個變數時用逗號分割。func專用基本公式有局部變數堆棧,結果公式可以引用形參變數,還可以引用上層變數堆棧中的外部變數,若函數返回的是數組或列表還可以採用省略寫法。例如:
func(x: 2*x) //單參數 func(x: x,2*x,3*x) //單參數、返回數組 func(x: x;2*x;3*x) //單參數、返回列表 func(x,y: (x+y)/2) //多參數
func專用基本公式可以把它賦值給變數後像系統函數那樣調用(參見前一章節),其實還可以直接進行調用如下。
func(x: 2*x)(2) //結果4 func(x: x,2*x,3*x)(2) //結果[2,4,6] func(x: x;2*x;3*x)(2) //結果{2,4,6} func(x,y: (x+y)/2)(2,5) //結果3.5
func(形參賦值序列 : 結果公式)
func專用擴展公式在func專用基本公式基礎上擴展,差別對形參進行了初始化賦值。其中每個形參賦值寫法為:變數名稱=變數公式,有多個變數時用逗號分割。例如:
func(x=5: 2*x) //單參數 func(x=5: x,2*x,3*x) //單參數、返回數組 func(x=5: x;2*x;3*x) //單參數、返回列表 func(x=5,y=10: (x+y)/2) //多參數
func擴展公式可以採用與func專用基本公式一樣的調用方式如下。
func(x=5: 2*x)(2) //結果4 func(x=5: x,2*x,3*x)(2) //結果[2,4,6] func(x=5: x;2*x;3*x)(2) //結果{2,4,6} func(x=5,y=10: (x+y)/2)(2,5) //結果3.5
func擴展公式還可以採用指定實參賦值的調用方式,註意由於實參調用有名字,因此賦值順序可以隨意,也可以預設,示例如下。
func(x=5: 2*x)(x=2) //結果4 func(x=5: x,2*x,3*x)(x=2) //結果[2,4,6] func(x=5: x;2*x;3*x)(x=2) //結果{2,4,6} func(x=5,y=10: (x+y)/2)(x=2,y=5) //結果3.5 func(x=5,y=10: (x+y)/2)(y=5,x=2) //結果3.5 func(x=5,y=10: (x+y)/2)(x=2) //結果6 func(x=5,y=10: (x+y)/2)(y=5) //結果5
1.2 func演算公式
func(形參序列) => 結果公式
func演算基本公式與func專用基本公式相似,差別主要是寫法上使用=>強調函數演算關係。註意1.2和1.3小節中的所有演算公式寫法在函數返回數組或列表時不可以採用省略寫法,必須顯式使用數組或列表的構造符號。例如:
func(x)=>2*x //單參數 func(x)=>[x,2*x,3*x] //單參數、返回數組 func(x)=>{x;2*x;3*x} //單參數、返回列表 func(x,y)=>(x+y)/2) //多參數
func演算基本公式的調用如下,註意函數定義體必須也加上()進行隔離,否則代碼含義不清。這種調用寫法,學習JavaScript的同學應該很熟悉,其中如果函數定義嵌套的層次深了,()就會更多,也會有LISP的即視感,哈!
(func(x)=>2*x)(2) //結果4 (func(x)=>[x,2*x,3*x])(2) //結果[2,4,6] (func(x)=>{x;2*x;3*x})(2) //結果{2,4,6} (func(x,y)=>(x+y)/2)(2,5) //結果3.5
func(形參賦值序列) => 結果公式
func演算擴展公式與func專用擴展公式相似,差別也是寫法上使用=>強調函數演算關係。例如:
func(x=5)=>2*x //單參數 func(x=5)=>[x,2*x,3*x] //單參數、返回數組 func(x=5)=>{x;2*x;3*x} //單參數、返回列表 func(x=5,y=10)=>(x+y)/2) //多參數
func演算擴展公式調用也要註意函數定義體必須也加上()進行隔離:
(func(x=5)=>2*x)(x=2) //結果4 (func(x=5)=>[x,2*x,3*x])(x=2) //結果[2,4,6] (func(x=5)=>{x;2*x;3*x})(x=2) //結果{2,4,6} (func(x=5,y=10)=>(x+y)/2)(x=2,y=5) //結果3.5 (func(x=5,y=10)=>(x+y)/2)(y=5,x=2) //結果3.5 (func(x=5,y=10)=>(x+y)/2)(x=2) //結果6 (func(x=5,y=10)=>(x+y)/2)(y=5) //結果5
1.3 簡化演算公式
單個形參 => 結果公式
單參簡化演算公式與func演算基本公式相似,差別主要是寫法上只支持單個參數,表達上更加精簡,有C# lambada 單參數表達式的即視感。例如:
x=>2*x //單參數 x=>[x,2*x,3*x] //單參數、返回數組 x=>{x;2*x;3*x} //單參數、返回列表
單參簡化演算公式的調用也要註意函數定義體必須也加上()進行隔離:
(x=>2*x)(2) //結果4 (x=>[x,2*x,3*x])(2) //結果[2,4,6] (x=>{x;2*x;3*x})(2) //結果{2,4,6}
() => 結果公式
無參簡化演算公式與單參簡化演算公式相似,差別主要是用()表示該函數無形式參數,與C# lambada 無參數表達式也類似。例如:
()=>3*PI //單參數
註意S#的無參數匿名函數只有這麼一種寫法。無參簡化演算公式的調用也要註意函數定義體必須也加上()進行隔離:
(()=>3*PI)() //結果9.42477796076938
二、函數定義
由於函數是一等公民了,因此函數通常也可以像變數一樣進行賦值定義。S#有兩種函數定義形式,但是其函數調用方法是一樣的。
2.1 隱式函數定義
變數名稱=匿名函數
這種寫法最為靈活多變,因為匿名函數被看作普通數據,可以通過命名變數進行傳遞和使用,無論是傳給其他函數作為實參調用,或是作為其他函數的返回值,都沒有任何違和感覺。例如:
{ f = x => 2 * x , g = f(5) , //直接調用,g結果10 h = { a = f , b = a(5) } //變數傳遞後間接調用,b結果10 }
隱式函數定義可以使用在任何變數賦值公式中,而且函數變數的作用域還可以用專門語法進行修改。函數式編程的好多技巧就是通過匿名函數和隱式定義實現的,本文後面小節會專門論述。
2.2 顯式函數定義(註:只能使用在eval專用公式中)
函數名稱(形參序列) = 結果公式
函數定義基本公式與func專用基本公式相似,差別主要直接指定函數名稱,並用=替代=>,最終輸出時把語法解析樹入口節點的引用賦值給指定名稱的函數變數。註意2.2小節中的所有函數定義寫法在函數返回數組或列表時也不可以採用省略寫法,必須顯式使用數組或列表的構造符號。例如:
eval( f(x) = 2 * x , g = f(5) : //直接調用,g結果10 { a = f , b = a(5) } //變數傳遞後間接調用,b結果10 )
從上面例子可以看出,顯式函數定義寫法更加簡潔,但是函數變數的作用域只能按照預設的公開設置,不可以修改。
函數名稱(形參賦值序列) = 結果公式
函數定義擴展公式在函數定義基本公式基礎上擴展,差別對形參進行了初始化賦值。例如:
eval( f(x=1) = 2 * x , g = f(x=5) : //直接調用,g結果10 { a = f , b = a(x=5) } //變數傳遞後間接調用,b結果10 )
從上面例子可以看出,顯式函數定義寫法更加簡潔,但是函數變數的作用域只函數名稱() = 結果公式
無參函數定義公式更加簡潔,直接使用()。例如:
eval( f() = 2 * PI , g = f() : //直接調用,g結果6.2831853071795862 { a = f , b = a() } //變數傳遞後間接調用,b結果6.2831853071795862 )
三、函數式編程技巧
3.1 函數嵌套調用
函數嵌套調用其實就是公式的嵌套,就是一個函數可以嵌套調用相同函數的另一次調用的結果作為參數。例如:
eval( m(x) = x / 2 : //定義對折函數 m(m(m(10))) //三次對折後,結果1.25 )
函數嵌套調用使用上很簡單,考驗的是軟體對函數變數傳遞的處理。
3.2 遞歸函數調用
遞歸函數是指函數定義體內部也調用了函數自身,其實這就形成了迴圈的嵌套函數調用,因此通常必須明確指定終止條件,否則會進入死迴圈導致程式出錯。
eval( m(x) = if(x<2? x : m(x/2)) : //定義遞歸對折函數 m(10) //遞歸對折後,結果1.25 )
3.3 函數參數是函數(高階函數)
函數參數是函數其實就是一個函數變數作為另一個函數的參數進行傳遞。
eval( m(x)=x/2 , //定義對折函數 f(array,y)=each(k@array:y(k)) : //定義映射函數 f([10,20,30],m) //對數組映射對折,結果[5,10,15] )
上面是手工寫,如果使用系統方法調用就會更加簡潔,一句話搞定:
[10,20,30].Cast(x=>x/2) //映射元素,結果[5,10,15]
這裡的.Cast其實與其他C#的Convert,Python的map函數功能相同,可以接受函數作為參數。類似的函數還有:
[10,20,30].Where(x=>x%4==0) //過濾元素,結果[20] [10,20,30].Count(x=>x%4==0) //條件計數,結果1
列表也一樣,例如:
{10,20,30}.Cast(x=>x/2) //映射元素,結果{5,10,15} {10,20,30}.Where(x=>x%4==0) //過濾元素,結果{20} {10,20,30}.Count(x=>x%4==0) //條件計數,結果1
3.4 函數返回值是函數(閉包函數)
閉包是函數式編程的重要的語法結構,通常指能夠讀取其他函數內部變數的函數。很多編程語言只有函數內部的子函數才能讀取局部變數,所以閉包可以理解成“定義在一個函數內部的函數“。在本質上閉包是將函數內部和函數外部連接起來的橋梁。
eval( m() = eval(n=2: x=>x/n) : //註意n為局部變數,使用閉包函數訪問 m()(10) //調用閉包函數,打幾折由內部說了算,結果5 )
3.5 函數參數和返回值都是函數(高階閉包函數)
eval( g(f) = eval //傳入雙參函數
( x = 10 , y = 20 : n => n * f( x, y ) //返回單參函數 ) :
g(func(a,b)=>(a+b)/2)(10) //結果150 )
函數式編程的核心在於深入理解函數也是數據或變數的思想,只要你掌握其本質,那麼在各種複雜使用場景中就可以駕輕就熟。
站在函數式編程的頂峰,你會感嘆“會當凌絕頂,一覽眾山小”!
聲明:原創文章歡迎轉載,但請註明出處,https://www.cnblogs.com/ShoneSharp。
軟體: S#語言編輯解析運行器,運行環境.NET 4.0,綠色軟體,單個EXE直接運行,無副作用。網盤下載鏈接為https://pan.baidu.com/s/1dF4e08p