函数式编程中,一切皆为函数,这个函数一般不是类级别的,其可以保存在变量中,可以当做参数或返回值,是函数级别的抽象和重用,将函数作为可重用的基本模块,就像面向对象中一切皆为对象,把所有事物抽象为类,面向对象编程通过继承和组合来实现类或模块重用,而函数式编程通过局部套用来实现函数重用;两种编程模式相辅相成,各有侧重点。函数式编程涉及高阶函数,纯函数、引用透明、闭包、局部套用、部分应用、惰性求值、单子等概念。
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中:
示例四: 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(_));