函數式編程中,一切皆為函數,這個函數一般不是類級別的,其可以保存在變數中,可以當做參數或返回值,是函數級別的抽象和重用,將函數作為可重用的基本模塊,就像面向對象中一切皆為對象,把所有事物抽象為類,面向對象編程通過繼承和組合來實現類或模塊重用,而函數式編程通過局部套用來實現函數重用;兩種編程模式相輔相 ...
函數式編程中,一切皆為函數,這個函數一般不是類級別的,其可以保存在變數中,可以當做參數或返回值,是函數級別的抽象和重用,將函數作為可重用的基本模塊,就像面向對象中一切皆為對象,把所有事物抽象為類,面向對象編程通過繼承和組合來實現類或模塊重用,而函數式編程通過局部套用來實現函數重用;兩種編程模式相輔相成,各有側重點。函數式編程涉及高階函數,純函數、引用透明、閉包、局部套用、部分應用、惰性求值、單子等概念。
C#不是函數式程式設計語言,但是隨著委托、lambda表達式、擴展方法、Linq、並行庫的引入,不斷方便我們進行函數式程式設計,另外,Monads.net庫也方便我們進行函數式編程。
一、函數式編程基本概念
1、高階函數
以函數為參數或返回結果的函數,如一個排序函數,其能適用於各種類型的數據,其排序邏輯一樣,但是不同數據類型的值比較方法不一樣,把比較函數當做參數,傳遞給排序函數。另外,C# 中Enumerable類中的Where、Select、SelectMany、First擴展方法都是高階函數。
2、引用透明/純函數
一個函數返回值,只取決於傳遞給它的參數,程式狀態通常不會影響函數返回值,這樣的函數稱為純函數,其沒有副作用,副作用即多個方法或函數共用訪問同一數據,函數式程式設計的主要思想之一就是控制這樣的副作用。
3、變數不變性
變數分局部變數(方法或類實例的局部變數) 全局變數(類的靜態欄位);變數是可變的,函數式程式設計並不歡迎程式中可變值的想法,變數值越公開帶來的問題越嚴重,一般原則是變數的值最好保持不變或在最小的作用域內保存其值,純函數最好只使用在自己模塊中定義的變數值,不訪問其作用域之外的任何變數。
4、閉包
當函數可以當成參數和返回值在函數之間傳遞時,編譯器利用閉包擴展變數的作用域,以保證隨時能得到所需要數據;局部套用(currying或加里化)和部分應用依賴於閉包。
static Func<int, int> GetClosureFunction()
{
//局部變數
int val = 10;
//局部函數
Func<int, int> internalAdd = x => x + val;
Console.WriteLine(internalAdd(10));//輸出20
val = 30;
//局部變數的改變會影響局部函數的值,即使變數的改變在局部函數創建之後。
Console.WriteLine(internalAdd(10));//輸出40
return internalAdd;
}
static void Closoures()
{
Console.WriteLine(GetClosureFunction()(30));//輸出60
}
局部變數val 作用域應該只在GetClosureFunction函數中,局部函數引用了外層作用域的變數val,編譯器為其創建一個匿名類,並把局部變數當成其中一個欄位,併在GetClosureFunction函數中實例化它,變數的值保存在欄位內,併在其作用域範圍外繼續使用。
5、局部套用或函數柯里化
函數柯里化是一種使用單參數函數來實現多參數函數的方法
多參函數:
Func<int, int, int> add = (x, y) => x + y;
單參數函數:
Func<int, Func<int, int>> curriedAdd = x => (y => x + y);
調用:curriedAdd (5)(3)
應用場景:預計算,記住前邊計算的值避免重覆計算
static bool IsInListDumb<T>(IEnumerable<T> list, T item)
{
var hashSet = new HashSet<T>(list);
return hashSet.Contains(item);
}
調用:
IsInListDumb(strings, "aa");
IsInListDumb(strings, "aa");
改造後:
static Func<T, bool> CurriedIsInListDumb<T>(IEnumerable<T> list)
{
var hashSet = new HashSet<T>(list);
return item => hashSet.Contains(item);
}
調用:
var curriedIsInListDumb = CurriedIsInListDumb(strings);
curriedIsInListDumb("aa");
curriedIsInListDumb ("bb");
6、部分應用或偏函數應用
找一個函數,固定其中的幾個參數值,從而得到一個新的函數;通過局部套用實現。
static void LogMsg(string range, string message)
{
Console.WriteLine($"{range} {message}");
}
//固化range參數
static Action<string> PartialLogMsg(Action<string, string> logMsg, string range)
{
return msg => logMsg(range, msg);
}
static void Main(string[] args)
{
PartialLogMsg(LogMsg, "Error")("充值失敗");
PartialLogMsg(LogMsg, "Warning")("金額錯誤");
}
部分應用例子:
代碼重覆版本:
using(var trans = conn.BeginTransaction()){
ExecuteSql(trans, "insert into people(id, name)value(1, 'Harry')");
ExecuteSql(trans, "insert into people(id, name)value(2, 'Jane')");
...
trans.Commit();
}
優化1:函數級別模塊化
using(var trans = conn.BeginTransaction()){
Action<SqlCeTransaction, int, string> exec = (transaction, id, name) =>
ExecuteSql(transaction, String.Format(
"insert into people(id, name)value({0},'{1}'", id, name));
exec (trans, 1, 'Harry');
exec (trans, 2, 'Jane');
...
trans.Commit();
}
優化2:部分應用
using(var trans = conn.BeginTransaction()){
Func<SqlCeTransaction, Func<int, Action<string>> exec = transaction => id => name =>
ExecuteSql(transaction, String.Format(
"insert into people(id, name)value({0},'{1}'", id, name)))(trans);
exec (1)( 'Harry');
exec (2)( 'Jane');
...
trans.Commit();
}
優化3:直接通過閉包簡化
using(var trans = conn.BeginTransaction()){
Action<SqlCeTransaction, int, string> exec = ( id, name) =>
ExecuteSql(trans , String.Format(
"insert into people(id, name)value({0},'{1}'", id, name));
exec (1, 'Harry');
exec ( 2, 'Jane');
...
trans.Commit();
}
7、惰性求值/嚴格求值
表達式或表達式的一部分只有當真正需要它們的結果時才對它們求值,嚴格求值指表達式在傳遞給函數之前求值,惰性求值的優點是可以提高程式執行效率,複雜演算法中很難決定某些操作執行還是不執行。
如下例子:
static int BigCalculation()
{
//big calculation
return 10;
}
static void DoSomething(int a, int b)
{
if(a != 0)
{
Console.WriteLine(b);
}
}
DoSomething(o, BigCalculation()) //嚴格求值
static HigherOrderDoSomething(Func<int> a, Func<int> b)
{
if(a() != 0)
{
Console.WriteLine(b());
}
}
HigherOrderDoSomething(() => 0, BigCalculation)//惰性求值
這也是函數式編程的一大好處。
8、單子(Monad)
把相關操作按某個特定類型鏈接起來。代碼更易閱讀,更簡潔,更清晰。
Monads.net是GitHub上一個開源的C#項目,提供了許多擴展方法,以便能夠在C#編程時編寫函數式編程風格的代碼。主要針對class、Nullable、IEnuerable以及Events類型提供
一些擴展方法。地址:https://github.com/sergeyzwezdin/monads.net。下麵舉些例子:
示例一: 使用With擴展方法獲取某人工作單位的電話號碼
var person = new Person();
var phoneNumber = "";
if(person != null && person.Work != null && person.Work.Phone != null)
{
phoneNumber = person.Work.Phone.Number;
}
在Monads.net中:
var person = new Person();
var phoneNumber = person.With(p => p.Work).With(w => w.Phone).With(p => p.Number);
代碼中主要使用了With擴展方法, 源代碼如下:
public static TResult With<TSource, TResult>(this TSource source, Func<TSource, TResult> action)
where TSource : class
{
if ((object) source != (object) default (TSource))
return action(source);
return default (TResult);
}
person.With(p => p.Work)這段代碼首先判斷person是否為空,如果不為Null則調用p => p.Work返回Work屬性,否則返回Null。
接下來With(w => w.Phone), 首先判斷上一個函數返回值是否為Null,如果不為Null則調用w => w.Phone返回Phone屬性,否則返回Null。
由此可以看出, 在上面的With函數調用鏈上任何一個With函數的source參數是Null,則結果也為Null, 這樣不拋出NullReferenceException。
示例二: 使用Return擴展方法獲取某人工作單位的電話號碼
在示例一中,如果person,Work,Phone對象中任一個為Null值phoneNumber會被賦於Null值。如果在此場景中要求phoneNumber不能Null,而是設置一個預設值,應該怎麼辦?
var person = new Person();
var phoneNumber = person.With(p => p.Work).With(w => w.Phone).Return(p => p.Number, defaultValue:"11111111");
當調用Return方法的source參數為Null時被返回。
示例三: Recover
Person person = null;
//person = new Person();
if(null == person)
{
person = new Person();
}
在Monads.net中:
Person person = null; //person = new Person(); person.Recover(p => new Person());示例四: try/catch
Person person = null;
try {
Console.WriteLine(person.Work);
} catch(NullReferenceException ex)
{
Console.WriteLine(ex.message);
}
在Monads.net中:
Person person = null;
person.TryDo(p => Console.WriteLine(p.Work), typeof(NullReferenceException)).Catch(ex => Console.WriteLine(ex.Message));
//忽略異常
Person person=null;
try {
Console.WriteLine(person.Work);
} catch()
{
}
在Monads.net中:
person.TryDo(p=>Console.WriteLine(p.Work)).Catch();
示例五: Dictionary.TryGetValue
var data = new Dictionary<int,string>();
string result = null;
if(data.TryGetValue(1, out result))
{
Console.WriteLine($"已找到Key為1的結果:{result}");
}else
{
Console.WriteLine($"未找到Key為1的結果");
}
在Monads.net中:
data.With(1).Return(_ => $"已找到Key為1的結果:{_}", "未找到Key為1的結果").Do(_ => Console.WriteLine(_));