如果你也会C#,那不妨了解下F#(4):了解函数及常用函数
函數式編程其實就是按照數學上的函數運算思想來實現計算機上的運算。雖然我們不需要深入了解數學函數的知識,但應該清楚函數式編程的基礎是來自于數學。
例如數學函數f(x) = x^2+x,并沒有指定返回值的類型,在數學函數中并不需要關心數值類型和返回值。F#代碼為let f x = x ** 2.0 + x,F#代碼和數學函數非常類似,其實這就是函數式編程的思想:只考慮用什么進行計算以及計算的結果(或者叫“輸入和輸出”),并不考慮怎樣計算。
其實,你可以把任何程序看成是一系列函數,輸入是你鼠標和鍵盤的操作,輸出是程序的運行結果。你不需要關心程序是怎樣運行的,這些函數會根據你的輸入來輸出結果,而其中的算法是以函數的形式,而不是類或者對象。
下面我們就先了解一些函數式編程中函數相關的東西。
了解函數
不可變性
在一個函數中改變了程序的狀態(比如在文件中寫入數據或者在內存中改變了全局變量)我們稱為副作用。像我們使用printfn函數,無論輸入是什么,返回值均為unit,但它的副作用是打印文字到屏幕上了。
副作用并不一定不好,但卻經常是很多bug的根源。我們分別用命令式和函數式求一組數字的平方和:
let square x = x * xlet sum1 nums = ? ?let mutable total = 0for i in nums do let x = square itotal <- total + xtotallet sum2 nums = ? ?Seq.sum (Seq.map square nums)在sum2中使用了Seq模塊中的函數,這些函數將在稍候進行介紹。
可以看出,函數式代碼簡短了許多,且少了很多變量的聲明。而且sum1是順序執行,若想以并行方式運行則需要更改所有代碼,但sum2只需要替換其中的Seq.sum和Seq.map函數。
函數和值
在我們接觸到的非函數式編程語言(包括C#)中,函數和數值總是有一些不同。但在函數式編程語言中,函數也是值。比如,函數可以作為其他函數的參數,也可以作為返回值(即高階函數)。而這在函數式編程中是非常常見的。
需要注意的是,我們叫“值”而不叫“變量”。因為在函數式編程中聲明的東西默認是不可變的。(在F#中不完全如此,是因為F#包含了面向對象編程范式,可以說并非純函數式編程語言。)
我們看下面以函數作為參數的代碼(求一組數字的負值):
> let negate x = -x;; val negate : x:int -> int> List.map negate [1..5];; val it : int list = [-1; -2; -3; -4; -5]我們使用函數negate和列表[1..5]作為List.map的參數。
但很多時候我們不需要給函數一個名稱,只需使用匿名函數或叫Lambda表達式。在F#中,Lambda表達式為:關鍵字fun和參數,加上箭頭->和函數體。則上面的代碼可以更改為:
List.map (fun i-> -i) [1..5];;我們再看以函數作為返回值的例子,假設我們定義一個powOf函數,輸入一個值,返回一個該值求冪的函數:
let powOf baseValue =(fun exp -> baseValue ** exp)let powOf2 = powOf 2.0 ?// f(x) = 2^xlet powOf3 = powOf 3.0 ?// f(x) = 3^xpowOf2 8. ? ? ? ? ? ? ? // 256.0powOf3 8. ? ? ? ? ? ? ? // 6561.0其中powOf2即為powOf函數使用參數2返回的函數。其實這里涉及到閉包的內容,就不詳細解釋了,我們詳細函數式編程時可能會再提及。
遞歸
遞歸大家都熟悉,只是在F#中聲明時,需要添加rec關鍵字:
let rec fact x = ? ?if x <= 1 then 1else x * fact (x-1) fact 5;;(* ? ?val fact : x:int -> int ? ?val it : int = 120 *)其實需要顯示聲明遞歸是因為F#的類型推斷系統無法在函數聲明完成之前確定其類型,而使用rec關鍵字后,就允許在確定類型前調用該函數。
部分函數:Partial Function
在函數式編程中,還有一個叫Partial Function(暫且叫部分函數吧)的,可以把接收多個參數的函數分解成接收單個參數,即柯里化(Currying)。
我們知道,使用函數printfn打印整數的語句為printfn "%d" i,我們定義一個打印整數的函數:
> let printInt i = printfn "%d" i;; val printInt : i:int -> unit > let printInt = printfn "%d";; val printInt : (int -> unit)符號函數
在F#中,如+ - * /等運算符其實屬于內建函數。而我們也可以使用這些符號來自定義符號函數。
我們用符號來重新定義上面的階乘函數:
let rec (!) x = ? ?if x <= 1 then 1else x * !(x - 1) !5;;(* ? ?val ( ! ) : int -> int ? ?val it : int = 120 *)需要注意的是,符號函數一般需要括號包裹,如果符號函數的參數不止一個,則符號函數是以中綴的方式來使用,例如我們用=~=定義一個驗證字符串是否和正則表達式匹配的函數:
open System.Text.RegularExpressions;;let (=~=) str (regex : string) =Regex.Match(str, regex).Success"The quick brown fox" =~= "The (.*) fox";; (*val ( =~= ) : string -> string -> boolval it : bool = true*)而且,符號函數也可以作為高階函數的參數。
管道符:|>和<|
我們再返回來看上面的平方和函數:
let sum2 nums = ? ?Seq.sum (Seq.map square nums)假如函數層次非常多,一層包裹一層,則可讀性非常差。
在F#定義了如下符號函數
let (|>) x f = f xlet (<|) f x = f x我們稱為“正向管道符”和“逆向管道符”。則上面的平方和函數可寫作:
let sum2 nums = nums |> Seq.map square |> Seq.sum<|雖然用得不多,但常用來改變優先級而無需使用括號:
let sum2 nums = ? ?Seq.sum <| Seq.map square nums合成符:>>和<<
我們也可以用函數合成符將多個函數組合成一個函數,合成符也分正向(>>)和逆向(<<)。
let (>>) f g x = g(f x) let (<<) f g x = f(g x)還是以上面的求平方和為例(Seq.map square即是一個部分函數):
let sum2 nums = (Seq.map square >> Seq.sum) numslet sum2 nums = (Seq.sum << Seq.map square) nums常用模塊函數
在上一篇中,我們了解了集合類型。在F#中,為這些集合類型定義了許多函數,分別在集合名稱對應的模塊中,例如Seq的相關函數位于模塊Microsoft.FSharp.Collections.Seq中。而這也是我們最常用到的模塊。
模塊(module)是F#中組織代碼的一種方式,類似于命令空間(namespace)。但F#中也是有命名空間的,其間的區別將在下一篇介紹。
下面簡單介紹常用的函數,并會列出與.Net的System.Linq中對應的函數。
如無特別說明,該函數在三個模塊中均可用,但因為集合的實現方式不同,函數的復雜度也會有區別,在使用中根據實際情況選擇合適的函數。
length
對應于Linq中的Count。即獲得集合中元素的個數。
[1..10] |> List.length;; ? ?// 10Seq.length {1..100};; ? ? ? // 100雖然在Seq中也有length函數,但謹慎使用,因為Seq可能為無限序列。
exists 和 exists2
exists用于判斷集合是否存在符合給定條件的元素,對應于Linq中的Any。而exists2用于判斷兩個集合是否包含在同一位置且符合給定條件的一對元素。
List.exists ((=) 3) [1;3;5;7];; ? ? //trueSeq.exists (fun n1 n2 -> n1=n2) {1..5} {5..-1..1};; //true第一行代碼判斷列表中是否包含等于3的元素,其中(=) 3即為部分函數,注意=為符號函數。
第二行代碼判斷兩個序列中,因為{1;2;3;4;5}和{5;4;3;2;1}在索引2的位置存在元素符合函數(fun n1 n2 -> n1=n2),所以返回true。
forall 和 forall2
forall檢查是否集合中所有元素均滿足指定條件,對應Linq中的All。
let nums = {2..2..10} nums |> Seq.forall (fun n -> n % 2 = 0);; ? //true而forall2和exists2類似,但當且僅當所有元素都滿足相同位置且符合給定條件才返回true。接上一個代碼片段:
let nums2 = {12..2..20} Seq.forall2 (fun n n2 -> n + 10 = n2) nums nums2;; ?//truefind 和 findIndex
find查找符合條件的第一個元素,對應Linq中的First。需要注意的是當不存在符合條件的元素,將引發KeyNotFoundException異常。
Seq.find (fun i -> i % 5 = 0) {1..100};; ? ?//5findIndex則返回符合條件的第一個元素的索引。
map 和 mapi
map對應Linq中的Select,將函數應用于集合中的每個元素,返回值產生一個新的集合。
List.map ((*) 2) [1..10];; ?// [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]mapi與map類似,不過在應用的函數中還需要傳入一個整數作為集合的索引。
Seq.mapi(fun i x -> x*i) [3;5;7;8;0];; // 將各個元素乘以各自的索引,結果為:[0; 5; 14; 24; 0]iter 和 iteri
iter將函數應用于集合中的每個元素,但函數返回值為unit。功能類似于for循環。
而iteri與mapi一樣需要在函數中傳入一個索引。
filter 和 where
F#中filter和where是一樣的,對應于Linq中的Where。用于查找符合條件的元素。
{1..10} |> Seq.filter (fun n -> n%2 = 0);;//val it : seq<int> = seq [2; 4; 6; 8; ...]fold
fold對應Linq中的Aggregate,通過提供初始值,然后將函數逐個應用于每個元素,返回單一值。
Seq.fold (fun acc n -> acc + n) 0 {1..5};; ?//15Seq.fold (fun acc n -> acc + string n) "" {1..10};; //"12345"首先,將初始值與第一個元素應用于函數,再將返回值與第二個元素應用于函數,依此類推……
Linq中的Aggregate包含不需要提供初始值的重載,其實F#中也有對應的reduce函數。類似的還有foldBack和reduceBack等逆向操作,這里就不介紹了。
collect
collect對應Linq中的SelectMany,展開集合并返回所有二級集合的元素。
let lists = [ [0;1]; [0;1;2]; [0;1;2;3] ] lists |> List.collect id;; //[0; 1; 0; 1; 2; 0; 1; 2; 3]其中id為Operators模塊中的函數,它的實現為fun n->n,即直接對參數進行返回。
append
append將兩個集合類型合并成一個,對應于Linq中的Concat。
> Array.append [|1;3;1;4|] [|5;2;0|];; val it : int [] = [|1; 3; 1; 4; 5; 2; 0|]zip 和 zip3
zip函數將兩個集合合并到一個里,合并后每個元素是一個二元元組。
let list1 = [ 1..3 ] let list2 = [ "a";"b";"c" ]List.zip list1 list2;; // [(1, "a"); (2, "b"); (3, "c")]zip3顧名思義,就是將三個集合合并到一個里。
合并后的長度取決于最短的集合的長度。
rev
rev函數反轉一個列表或數組,在Seq模塊中沒有這個函數。
sort
sort函數基于compare函數(第二篇中的“比較”介紹過)對集合中的元素進行排序。
> List.sort [1;3;-2;2];; val it : int list = [-2; 1; 2; 3]數學函數
Linq中包含Max、Min、Average和Sum等數學函數。F#集合模塊中也有對應的函數。
List.max [1..10] ? ? ? ?//10Seq.min {1..5} ? ? ? ? ?//5[1..10] |> List.map float |> List.average ? //5.5List.averageBy float [1..10] ? ? ? ? ? ? ? ?//5.5[0..100] |> Seq.where (fun x -> x % 2 <> 0) |> Seq.sum |> printf "0到100中的奇數的和為%i"// 0到100中的奇數的和為2500需要注意的是,average函數需要集合中的元素支持精確除法(Exact division,即實現了DivideByInt函數的類型。不知道為什么是ByInt。),而F#中又不支持隱式類型轉換,所以對int集合求平均值只能先轉換為float或float32,或使用averageBy函數。
sum函數的示例代碼將第一篇中由C#翻譯過來的命令示示例代碼轉換成了函數式的代碼。
集合間轉換
三種集合類型的對應模塊中,均提供轉換到(to)另外兩種集合類型,和從(of)另外兩種類型轉換的函數。
如Seq模塊,通過Seq.toList和Seq.toArray函數轉出;通過Seq.ofList和Seq.ofArray轉入。
Seq.toList {1..5};; ? ? ? ? //[1; 2; 3; 4; 5]List.ofArray [|1..5|];; ? ? //[1; 2; 3; 4; 5]函數式編程,核心就是函數的運用。上面介紹的這些在C#中也經常使用到對應的方法,但F#提供的函數非常豐富,大家可通過MSDN了解更多:
Seq模塊
List模塊
Array模塊
因為F#中的List和Array均實現了IEnumarable<T>接口,所以Seq模塊的函數也可以接收List類型和Array類型的參數。當然,反之則不行。
到現在為止,我們了解的F#都是在交互窗口中。下一篇我們再簡單介紹項目創建和代碼組織,即模塊相關。
相關文章:
如果你也會C#,那不妨了解下F#(1):F# 數據類型
如果你也會C#,那不妨了解下F#(2):數值運算和流程控制語法
如果你也會C#,那不妨了解下F#(3):F#集合類型和其他核心類型
【送書活動】機器學習項目開發實戰
《機器學習項目開發實戰》送書活動結果公布
原文地址:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-4.html
.NET社區新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關注
總結
以上是生活随笔為你收集整理的如果你也会C#,那不妨了解下F#(4):了解函数及常用函数的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 升讯威微信营销系统开发教程:(1)订阅号
- 下一篇: 先定个小目标, 使用C# 开发的千万级应