When mentioning functional programming, everyone must think of ancient functional languages such as LISP and Haskell, which have highly flexible and dynamic syntax. More recently, Ruby, JavaScript, and F# are also popular languages for functional programming. However, since .net supports lambda expressions, although C# is an imperative programming language, it is not inferior in functional programming. In the process of writing code in C#, we will use ideas such as higher-order functions, combined functions, and pure function caching intentionally or unintentionally. Even ideas such as expression trees come from functional programming ideas. So next we will summarize the commonly used functional programming scenarios, which will help us flexibly apply these technologies in the programming process, expand our design ideas and improve code quality.
In layman's terms, higher-order functions: a function that uses a function as a parameter is called a higher-order function. According to this definition, LINQ expressions, Where, Select, SelectMany, First and other methods used extensively in .net are all high-order functions. So when will we use this design when we write our own code?
Example: Design a function to calculate property fees, var fee=square*price, and the area (square) is calculated in different ways depending on the nature of the property. Civilian residences, commercial residences, etc. need to be multiplied by different coefficients. According to such needs, we try to design the following function:
Residential area:
public Func<int,int,decimal> SquareForCivil() { return (width,hight)=>width*hight; }
Commercial residential area:
public Func<int, int, decimal> SquareForBusiness() { return (width, hight) => width * hight*1.2m; }
These functions all have a common signature: Func
public decimal PropertyFee(decimal price,int width,int hight, Func<int, int, decimal> square) { return price*square(width, hight); }
Isn’t it very easy? Write a test and see
[Test] public void Should_calculate_propertyFee_for_two_area() { //Arrange var calculator = new PropertyFeeCalculator(); //Act var feeForBusiness= calculator.PropertyFee(2m,2, 2, calculator.SquareForBusiness()); var feeForCivil = calculator.PropertyFee(1m, 2, 2, calculator.SquareForCivil()); //Assert feeForBusiness.Should().Be(9.6m); feeForCivil.Should().Be(4m); }
C# uses a strict evaluation strategy during execution. The so-called strict evaluation means that parameters are evaluated before being passed to the function. Is this explanation still a little unclear? Let's look at a scenario: there is a task that needs to be executed, which requires that the current memory usage is less than 80%, and the result of the previous calculation is <100. This condition can only be executed if this condition is met.
We can quickly write C# code that meets this requirement:
public double MemoryUtilization() { //计算目前内存使用率 var pcInfo = new ComputerInfo(); var usedMem = pcInfo.TotalPhysicalMemory - pcInfo.AvailablePhysicalMemory; return (double)(usedMem / Convert.ToDecimal(pcInfo.TotalPhysicalMemory)); } public int BigCalculatationForFirstStep() { //第一步运算 System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine("big calulation"); FirstStepExecuted = true; return 10; } public void NextStep(double memoryUtilization,int firstStepDistance) { //下一步运算 if(memoryUtilization<0.8&&firstStepDistance<100) { Console.WriteLine("Next step"); } }
When executing NextStep, you need to pass in the memory usage and the calculation result of the first step (function BigCalculationForFirstStep). As shown in the code, the first step operation is a very time-consuming operation, but due to the strict evaluation strategy of C#, for statements If (memoryUtilization<0.8&&firstStepDistance<100), even if the memory usage is greater than 80%, the first step operation must be performed. Obviously, if the memory usage is greater than 80%, the value of firstStepDistance is no longer important, and it is completely fine. No need to calculate.
So lazy evaluation means that expressions or parts of expressions are evaluated only when their results are actually needed. We try to rewrite this requirement using higher-order functions:
public void NextStepWithOrderFunction(Func<double> memoryUtilization,Func<int> firstStep) { if (memoryUtilization() < 0.8 && firstStep() < 100) { Console.WriteLine("Next step"); } }
The code is very simple, just use a function expression to replace the function value. If if (memoryUtilization() < 0.8.. is not satisfied, the following functions will not be executed. Microsoft added Lazy< in version .net4.0 ;T> class, you can use this mechanism in scenarios where there is such a need.
Currying is also called partial application. Definition: It is a technology that transforms a function that accepts multiple parameters into a function that accepts a single parameter (the first parameter of the original function), and returns a new function that accepts the remaining parameters and returns a result. PS: Why is the official explanation so confusing? mouth?
Seeing such a definition, it is probably difficult for everyone to understand what this is, so let’s start with the principle of curry:
Write a function that adds two numbers:
public Func<int, int, int> AddTwoNumber() { return (x, y) => x + y; }
OK, how to use this function?
var result= _curringReasoning.AddTwoNumber()(1,2);
1+2=3, the call is very simple. To upgrade our requirements, we need a function that requires the input of a parameter (number) and calculates the result of 10 + the input parameter (number). I guess someone has said that the above code can completely achieve this requirement. If you pass in 10 as the first parameter, it will be over. OK, if you think so, I have nothing to do. Others may have said that writing an overload requires only one parameter, but the actual situation is not allowed. We are calling an api provided by others and cannot add an overload. It can be seen that the usage scenario of partial application is not a very common scenario, so the best design is to match the appropriate technology in the appropriate scene. Let’s look at the implementation of partial application:
public Func<int, Func<int, int>> AddTwoNumberCurrying() { Func<int, Func<int, int>> addCurrying = x => y => x + y; return addCurrying; }
The function signature obtained by the expression x => y => Function of type int>. At this point if we call again:
//Act var curringResult = curringReasoning.AddTwoNumberCurrying()(10); var result = curringResult(2); //Assert result.Should().Be(12);
这句话:var curringResult = curringReasoning.AddTwoNumberCurrying()(10); 生成的函数就是只接收一个参数(number),且可以计算出10+number的函数。
同样的道理,三个数相加的函数:
public Func<int,int,int,int> AddThreeNumber() { return (x, y, z) => x + y + z; }
局部套用版本:
public Func<int,Func<int,Func<int,int>>> AddThreeNumberCurrying() { Func<int, Func<int, Func<int, int>>> addCurring = x => y => z => x + y + z; return addCurring; }
调用过程:
[Test] public void Three_number_add_test() { //Arrange var curringReasoning = new CurryingReasoning(); //Act var result1 = curringReasoning.AddThreeNumber()(1, 2, 3); var curringResult = curringReasoning.AddThreeNumberCurrying()(1); var curringResult2 = curringResult(2); var result2 = curringResult2(3); //Assert result1.Should().Be(6); result2.Should().Be(6); }
当函数参数多了之后,手动局部套用越来越不容易写,我们可以利用扩展方法自动局部套用:
public static Func<T1, Func<T2, TResult>> Curry<T1, T2, TResult>(this Func<T1, T2, TResult> func) { return x => y => func(x, y); } public static Func<T1, Func<T2, Func<T3, TResult>>> Curry<T1, T2, T3, TResult>(this Func<T1, T2, T3,TResult> func) { return x => y => z=>func(x, y,z); }
同样的道理,Action<>签名的函数也可以自动套用
有了这些扩展方法,使用局部套用的时候就更加easy了
[Test] public void Should_auto_curry_two_number_add_function() { //Arrange var add = _curringReasoning.AddTwoNumber(); var addCurrying = add.Curry(); //Act var result = addCurrying(1)(2); //Assert result.Should().Be(3); }
好了,局部套用就说到这里,stackoverflow有几篇关于currying使用的场景和定义的文章,大家可以继续了解。
函数式编程还有一些重要的思想,例如:纯函数的缓存,所为纯函数是指函数的调用不受外界的影响,相同的参数调用得到的值始终是相同的。尾递归,单子,代码即数据(.net中的表达式树),部分应用,组合函数,这些思想有的我也仍然在学习中,有的还在思考其最佳使用场景,所以不再总结,如果哪天领会了其思想会补充。
最后我还是想设计一个场景,把高阶函数,lambda表达式,泛型方法结合在一起,我之所以设计这样的例子是因为现在很多的框架,开源的项目都有类似的写法,也正是因为各种技术和思想结合在一起,才有了极富有表达力并且非常优雅的代码。
需求:设计一个单词查找器,该查找器可以查找某个传入的model的某些字段是否包含某个单词,由于不同的model具有不同的字段,所以该查找需要配置,并且可以充分利用vs的智能提示。
这个功能其实就两个方法:
private readonly List<Func<string, bool>> _conditions; public WordFinder<TModel> Find<TProperty>(Func<TModel,TProperty> expression) { Func<string, bool> searchCondition = word => expression(_model).ToString().Split(' ').Contains(word); _conditions.Add(searchCondition); return this; } public bool Execute(string wordList) { return _conditions.Any(x=>x(wordList)); }
使用:
[Test] public void Should_find_a_word() { //Arrange var article = new Article() { Title = "this is a title", Content = "this is content", Comment = "this is comment", Author = "this is author" }; //Act var result = Finder.For(article) .Find(x => x.Title) .Find(x => x.Content) .Find(x => x.Comment) .Find(x => x.Author) .Execute( "content"); //Assert result.Should().Be(true); }
该案例本身不具有实用性,但是大家可以看到,正是各种技术的综合应用才设计出极具语义的api, 如果函数参数改为Expression
The above is the detailed content of Detailed introduction to C# functional programming sample code. For more information, please follow other related articles on the PHP Chinese website!