59

拿 C# 搞函数式编程 - 1 - hez2010

 4 years ago
source link: https://www.cnblogs.com/hez2010/p/11487006.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

最近闲下来了,准备出一个 C# 搞 FP 的合集。本合集所有代码均以 C# 8 为示例。

可能你说,为什么要这么做呢?回答:为了好玩。另外,意义党们请 gun cu ke!

C# 有委托,而且有 Func<> 和 Action<>,可以说函数被视为一等功名,跟 int、bool 等类型并没有什么区别。那么很多事情就简单了。

纯函数#

什么是纯函数呢?纯函数就是 f(x),它们接收参数,得到结果,并且相同的参数得到的结果一定是相同的,用映射来说,它是满射的。另外这个函数不会改变任何的状态值,它是无副作用的。

柯里化#

首先,有一个东西让我觉得不爽,那就是一般来说 C# 里的函数调用不是柯里化的,这也就意味着我没法一个一个传参数进去,也没法把传了一部分参数的调用作为一个新函数拿去给别的地方用,那要怎么办呢?

自己动手,丰衣足食!

一个标准的加法函数可以这么写:

var function = new Func<int, int, int>
    ((x, y) => x + y);
function(1, 2); // returns 3

如果我们想以柯里化形式调用的话,理想状态是这么个样子的:

function 1 2

但是这个括号我们是省不了的,所以这样也是可以接受的:

function(1)(2);

我们看一下这个调用形式,不就是 Func<int, Func<int, int>> 嘛!so easy~

我们只需要把 Func<int, int, int> 转化为 Func<int, Func<int, int>>:

Func<int, Func<int, int>> Currying(Func<int, int, int> f) 
    => x => y => f(x, y);

这样写就 ok 啦。进一步改造成扩展方法:

public static class CurryingExtensions
{
    public static Func<int, Func<int, int>> 
        Currying(this Func<int, int, int> f) 
            => x => y => f(x, y);
}

于是我们只需要:

var function = new Func<int, int, int>
    ((x, y) => x + y)
    .Currying();
function(1)(2); // returns 3

就可以采用柯里化形式调用该函数啦。

进一步我们用泛型改造,让柯里化适用于任何类型:

public static class CurryingExtensions
{
    public static Func<T1, Func<T2, TOutput>> 
        Currying<T1, T2, TOutput>(this Func<T1, T2, TOuput> f)
            => x => y => f(x, y);
}

如果遇到更多参数,我们只需要给这个静态类里面再加一个扩展方法即可。

那 Action<> 呢?这个东西在我看来完全就是副作用,具体下方有讲,我们不用他(逃

Unit#

什么是 Unit 呢?Unit 就是任何函数调用后如果没有结果,就会返回的一个东西。

可能你说,void 不就可以了?

但是如果一个纯函数,它没有返回值(即 Action<>),意味着这个函数它有输入没输出,那这个函数除了能用来产生副作用之外,就什么都干不了了。这不清真!

因此我们需要一个 Unit 来代替 void,偷个懒,这个 Unit 就用 ulong 来代替吧。

高阶函数#

什么叫做高阶函数,把函数当作参数传给另一个函数,接收这个函数参数的函数就叫做高阶函数。

举个例子:f(g(x)),f 即高阶函数。

假设我们现在要开一个超市,超市有很多的产品,每种产品价格不同,不同产品可能还有各自的折扣。我们有很多种快乐水,每种快乐水价格不一样,可口快乐水 3.5 块,百事快乐水 3 块,麦当劳快乐水 9 块,快乐水价格计算函数:

var happyWater = new Func<float, int, float>
    ((float price, int number) => number * price)
    .Currying();
// 调用:happyWater(快乐水单价)(快乐水件数);

var cocaHappyWater = happyWater(3.5f);
var pepsiHappyWater = happyWater(3);
var mcdHappyWater = happyWater(9);

超市可能有折扣,A 超市不打折,B 超市打八折,计算价格函数:

var calcPrice = new Func<Func<int, float>, float, int, float>
    ((calc, discount, number) => discount * calc(number))
    .Currying();
// 调用:calcPrice(快乐水价格计算函数)(超市折扣)(快乐水件数);

现在我们分别在 A 超市买百事快乐水、B 超市买可口快乐水,麦当劳的太贵了我们不买,价格计算函数为:

var pepsiPriceCalc = calcPrice(pepsiHappyWater);
var cocaPriceCalc = calcPrice(cocaHappyWater);

var priceCalcA = pepsiPriceCalc(1); // A 超市
var priceCalcB = cocaPriceCalc(0.8f); // B 超市

最后我们在 A 超市买了 3 瓶百事快乐水,B 超市买了 5 瓶可口快乐水,计算总价:

var priceA = priceCalcA(3);
var priceB = priceCalcB(5);
var total = priceA + priceB;

最后得到 total = 23 元。

可以看到这些函数都是可拆卸并且可以随意组合的,而且满足 f(g(x)) = g(f(x))。

贴上完整代码示例:

using System;

namespace ColaMarket
{
    static class CurryingExtensions
    {
        public static Func<T1, Func<T2, TOutput>>
            Currying<T1, T2, TOutput>(this Func<T1, T2, TOutput> f)
                => x => y => f(x, y);

        public static Func<T1, Func<T2, Func<T3, TOutput>>>
            Currying<T1, T2, T3, TOutput>(this Func<T1, T2, T3, TOutput> f)
                => x => y => z => f(x, y, z);
    }

    class Program
    {
        static void Main(string[] args)
        {
            var happyWater = new Func<float, int, float>
                ((float price, int number) => number * price)
                .Currying();

            var cocaHappyWater = happyWater(3.5f);
            var pepsiHappyWater = happyWater(3);
            var mcdHappyWater = happyWater(9);

            var calcPrice = new Func<Func<int, float>, float, int, float>
                ((calc, discount, number) => discount * calc(number))
                .Currying();

            var pepsiPriceCalc = calcPrice(pepsiHappyWater);
            var cocaPriceCalc = calcPrice(cocaHappyWater);

            var priceCalcA = pepsiPriceCalc(1);
            var priceCalcB = cocaPriceCalc(0.8f);

            var priceA = priceCalcA(3);
            var priceB = priceCalcB(5);
            var total = priceA + priceB;

            Console.WriteLine(total);
        }
    }
}

下一篇将会讲更多的东西,如 Functor、Applicative 和 Monad 等等。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK