若是你也會C#,那不妨瞭解下F#(4):瞭解函數及經常使用函數

函數式編程其實就是按照數學上的函數運算思想來實現計算機上的運算。雖然咱們不須要深刻了解數學函數的知識,但應該清楚函數式編程的基礎是來自於數學。html

例如數學函數f(x) = x^2+x,並無指定返回值的類型,在數學函數中並不須要關心數值類型和返回值。F#代碼爲let f x = x ** 2.0 + x,F#代碼和數學函數很是相似,其實這就是函數式編程的思想:只考慮用什麼進行計算以及計算的結果(或者叫「輸入和輸出」),並不考慮怎樣計算。正則表達式

其實,你能夠把任何程序當作是一系列函數,輸入是你鼠標和鍵盤的操做,輸出是程序的運行結果。你不須要關心程序是怎樣運行的,這些函數會根據你的輸入來輸出結果,而其中的算法是以函數的形式,而不是類或者對象。算法

下面咱們就先了解一些函數式編程中函數相關的東西。編程


瞭解函數

不可變性

在一個函數中改變了程序的狀態(好比在文件中寫入數據或者在內存中改變了全局變量)咱們稱爲反作用。像咱們使用printfn函數,不管輸入是什麼,返回值均爲unit,但它的反作用是打印文字到屏幕上了。api

反作用並不必定很差,但卻常常是不少bug的根源。咱們分別用命令式和函數式求一組數字的平方和:數組

let square x = x * x
let sum1 nums =
    let mutable total = 0
    for i in nums do 
        let x = square i
        total <- total + x
    total
let sum2 nums =
    Seq.sum (Seq.map square nums)

sum2中使用了Seq模塊中的函數,這些函數將在稍候進行介紹。閉包

能夠看出,函數式代碼簡短了許多,且少了不少變量的聲明。並且sum1是順序執行,若想以並行方式運行則須要更改全部代碼,但sum2只須要替換其中的Seq.sumSeq.map函數。app

函數和值

在咱們接觸到的非函數式編程語言(包括C#)中,函數和數值老是有一些不一樣。但在函數式編程語言中,函數也是值。好比,函數能夠做爲其餘函數的參數,也能夠做爲返回值(即高階函數)。而這在函數式編程中是很是常見的。
須要注意的是,咱們叫「」而不叫「變量」。由於在函數式編程中聲明的東西默認是不可變的。(在F#中不徹底如此,是由於F#包含了面向對象編程範式,能夠說並不是純函數式編程語言。)編程語言

咱們看下面以函數做爲參數的代碼(求一組數字的負值):ide

> 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^x
let powOf3 = powOf 3.0	// f(x) = 3^x
powOf2 8.				// 256.0
powOf3 8.   			// 6561.0

其中powOf2即爲powOf函數使用參數2返回的函數。其實這裏涉及到閉包的內容,就不詳細解釋了,咱們詳細函數式編程時可能會再說起。

遞歸

遞歸你們都熟悉,只是在F#中聲明時,須要添加rec關鍵字:

let rec fact x =
    if x <= 1 then 1
    else 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 1
    else 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 -> bool
	val it : bool = true
*)

並且,符號函數也能夠做爲高階函數的參數

管道符:|><|

咱們再返回來看上面的平方和函數:

let sum2 nums =
    Seq.sum (Seq.map square nums)

假如函數層次很是多,一層包裹一層,則可讀性很是差。

在F#定義了以下符號函數

let (|>) x f = f x
let (<|) 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) nums
let 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;;	// 10
Seq.length {1..100};;		// 100

雖然在Seq中也有length函數,但謹慎使用,由於Seq可能爲無限序列。

exists 和 exists2

exists用於判斷集合是否存在符合給定條件的元素,對應於Linq中的Any。而exists2用於判斷兩個集合是否包含在同一位置且符合給定條件的一對元素。

List.exists ((=) 3) [1;3;5;7];;		//true
Seq.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

forall2exists2相似,但當且僅當全部元素都知足相同位置且符合給定條件才返回true。接上一個代碼片斷:

let nums2 = {12..2..20}
Seq.forall2 (fun n n2 -> n + 10 = n2) nums nums2;;	//true

find 和 findIndex

find查找符合條件的第一個元素,對應Linq中的First。須要注意的是當不存在符合條件的元素,將引起KeyNotFoundException異常。

Seq.find (fun i -> i % 5 = 0) {1..100};;	//5

findIndex則返回符合條件的第一個元素的索引

map 和 mapi

map對應Linq中的Select,將函數應用於集合中的每一個元素,返回值產生一個新的集合。

List.map ((*) 2) [1..10];;	
// [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]

mapimap相似,不過在應用的函數中還須要傳入一個整數做爲集合的索引。

Seq.mapi(fun i x -> x*i) [3;5;7;8;0];; 
// 將各個元素乘以各自的索引,結果爲:[0; 5; 14; 24; 0]

iter 和 iteri

iter將函數應用於集合中的每一個元素,但函數返回值爲unit。功能相似於for循環。 而iterimapi同樣須要在函數中傳入一個索引。

Seq.iteri(fun i x -> printfn "第%d個元素爲:%d" i x) [3;5;7;8;0]
(*
    第0個元素爲:3
    第1個元素爲:5
    ……
*)

filter 和 where

F#中filterwhere是同樣的,對應於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};;	//15
Seq.fold (fun acc n -> acc + string n) "" {1..10};;	
//"12345"

首先,將初始值與第一個元素應用於函數,再將返回值與第二個元素應用於函數,依此類推……

Linq中的Aggregate包含不須要提供初始值的重載,其實F#中也有對應的reduce函數。相似的還有foldBackreduceBack等逆向操做,這裏就不介紹了。

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]

其中idOperators模塊中的函數,它的實現爲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中包含MaxMinAverageSum等數學函數。F#集合模塊中也有對應的函數。

List.max [1..10]		//10
Seq.min {1..5}			//5
[1..10] |> List.map float |> List.average	//5.5
List.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集合求平均值只能先轉換爲floatfloat32,或使用averageBy函數。

sum函數的示例代碼將第一篇中由C#翻譯過來的命令示示例代碼轉換成了函數式的代碼。

集合間轉換

三種集合類型的對應模塊中,均提供轉換**到(to)另外兩種集合類型,和從(of)**另外兩種類型轉換的函數。

如Seq模塊,經過Seq.toListSeq.toArray函數轉出;經過Seq.ofListSeq.ofArray轉入。

Seq.toList {1..5};;			//[1; 2; 3; 4; 5]
List.ofArray [|1..5|];;		//[1; 2; 3; 4; 5]

函數式編程,核心就是函數的運用。上面介紹的這些在C#中也常用到對應的方法,但F#提供的函數很是豐富,你們可經過MSDN瞭解更多:

由於F#中的List和Array均實現了IEnumarable<T>接口,因此Seq模塊的函數也能夠接收List類型和Array類型的參數。固然,反之則不行。

到如今爲止,咱們瞭解的F#都是在交互窗口中。下一篇咱們再簡單介紹項目建立和代碼組織,即模塊相關。

本文發表於博客園。 原文連接爲:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-4.html
可前往博客園查看更多文章。

相關文章
相關標籤/搜索