拿 C# 搞函数式编程 - 2

前一阵子在写 CPU,致使一直没有什么时间去作其余的事情,如今好不容易作完闲下来了,我又能够水文章了哈哈哈哈哈。express

有关 FP 的类型部分我打算放到明年再讲,由于现有的 C# 虽然有一个 pattern matching expressions,可是没有 discriminated unions 和 records,只能说是个半残废,要实现 FP 那一套的类型异常的复杂。西卡西,discriminated unions 和 records 这两个东西官方已经定到 C# 9 了,因此等明年 C# 9 发布了以后我再继续说这部分的内容。数组

另外,conceptstype classes)、traits 、intersect & sum types 和高阶类型也可能会随着 C# 九、10 一并到来。所以到时候再讲才会讲得更爽。另外吹一波 traits类型系统,一样是图灵完备的类型系统,在表达力上要比OOP强太多,欢迎你们入坑,好比 Rust 和将来的 C#。app

这一部分咱们介绍一下 FunctorApplicative和 Monad 都是些什么。dom

本文试图直观地讲,目的是让读者能比较容易的理解,而不是准确知道其概念如何,所以会尽可能避免使用一些专用的术语,如范畴学、数学、λ 计算等等里面的东西。感兴趣的话建议参考其余更专业的资料。ide

Functor

Functor 也叫作函子。想象一下这样一件事情:函数

如今咱们有一个纯函数 IsOddthis

bool IsOdd(int value) => (value & 1) == 1;

这个纯函数只干一件事情:判断输入是否是奇数。spa

那么如今问题来了,若是咱们有一个整数列表,要怎么去作上面这件事情呢?code

可能会有人说这太简单了,这样就可:blog

var list = new List<int>();
return list.Select(IsOdd).ToList();

上面这句干了件什么事情呢?其实就是:咱们将 IsOdd 函数应用到了列表中的每个元素上,将产生的新的列表返回。

如今咱们作一次抽象,咱们将这个列表想象成一个箱子M,那么咱们的须要干的事情就是:把一个装着 A 类型东西的箱子变成一个装着 B 类型东西的箱子(AB类型可相同),即 fmap函数,而作这个变化的方法就是:进入箱子M,把里面的A变成B

它分别接收一个把东西从A变成B的函数、一个装着AM,产生一个装着BM

M<B> Fmap(this M<A> input, Func<A, B> func);

你暂且能够简单地认为,判断一个箱子是否是 Functor,就是判断它有没有 fmap这个操做。

Maybe

咱们应该都接触过 C# 的 Nullable<T>类型,好比 Nullable<int> t,或者写成 int? t,这个t,当里面的值为 null 时,它为 null,不然他为包含的值。

此时咱们把这个 Nullable<T>想象成这个箱子 M。那么咱们能够这么说,这个M有两种形式,一种是 Just<T>,表示有值,且值在 Just 里面存放;另外一种是 Nothing,表示没有值。

用 Haskell 写这个Nullable<T>类型定义的话,大概长这个样子:

data Nullable x = Just x | Nothing

而之因此这个Nullable<T>既多是 Nothing,又多是 Just<T>,只是由于 C# 的 BCL 中包含相关的隐式转换而已。

因为自带的 Nullable<T>不太好具体讲咱们的各类实现,且只接受值类型的数据,所以咱们本身实现一个Maybe<T>

public class Maybe<T> where T : notnull
{
    private readonly T innerValue;
    public bool HasValue { get; } = false;
    public T Value => HasValue ? innerValue : throw new InvalidOperationException();

    public Maybe(T value)
    {
        if (value is null) return;
        innerValue = value;
        HasValue = true;
    }

    public Maybe(Maybe<T> value)
    {
        if (!value.HasValue) return;
        innerValue = value.Value;
        HasValue = true;
    }

    private Maybe() { }

    public static implicit operator Maybe<T>(T value) => new Maybe<T>(value);
    public static Maybe<T> Nothing() => new Maybe<T>();
    public override string ToString() => HasValue ? Value.ToString() : "Nothing";
}

对于 Maybe<T>,咱们能够写一下它的 fmap函数:

public static Maybe<B> Fmap<A, B>(this Maybe<A> input, Func<A, B> func)
    => input switch
    {
        null => Maybe<B>.Nothing(),
        { HasValue: true } => new Maybe<B>(func(input.Value)),
        _ => Maybe<B>.Nothing()
    };

Maybe<int> t1 = 7;
Maybe<int> t2 = Maybe<int>.Nothing();
Func<int, bool> func = x => (x & 1) == 1;
t1.Fmap(func); // Just True
t2.Fmap(func); // Nothing

Applicative

有了上面的东西,如今咱们说说 Applicative 是干什么的。

你能够很是容易的发现,若是你为 Maybe<T>实现一个 fmap,那么你能够说 Maybe<T>就是一个 Functor

那 Applicative 也差很少,首先Applicative是继承自Functor的,因此Applicative自己就具备了 fmap。另外在 Applicative中,咱们有两个分别叫作pure和 apply的函数。

pure干的事情很简单,就是把东西装到箱子里:

M<T> Pure<T>(T input);

那 apply 干了件什么事情呢?想象一下这件事情,此时咱们把以前所说的那个用于变换的函数(Func<A, B>)也装到了箱子当中,变成了M<Func<A, B>>,那么apply所作的就是下面这件事情:

M<B> Apply(this M<A> input, M<Func<A, B>> func);

看起来和 fmap没有太大的区别,惟一的不一样就是咱们把func也装到了箱子M里面。

以 Maybe<T>为例实现 apply

public static Maybe<B> Apply<A, B>(this Maybe<A> input, Maybe<Func<A, B>> func)
    => (input, func) switch
    {
        _ when input is null || func is null => Maybe<B>.Nothing(),
        ({ HasValue: true }, { HasValue: true }) => new Maybe<B>(func.Value(input.Value)),
        _ => Maybe<B>.Nothing()
    };

而后咱们就能够干这件事情了:

Maybe<int> input = 3;
Maybe<Func<int, bool>> isOdd = new Func<int, bool>(x => (x & 1) == 1);

input.Apply(isOdd); // Just True

咱们的这个函数 isOdd自己多是 Nothing,当 inputisOdd任何一个为Nothing的时候,结果都是Nothing,不然是Just,而且将值存到这个 Just里面。

Monad

Monad 继承自 Applicative,并另外包含几个额外的操做:returnsbindthen

returns干的事情和上面的Applicativepure干的事情没有区别。

public static Maybe<A> Returns<A>(this A input) => new Maybe<A>(input);

bind干这么一件事情 :

M<B> Bind<A, B>(this M<A> input, Func<A, M<B>> func);

它用一个装在 M中的A,和一个A -> M<B>这样的函数,产生一个M<B>

then用来充当胶水的做用,将一个个操做链接起来:

M<B> Then(this M<A> a, M<B> b);

为何说这是充当胶水的做用呢?想象一下若是咱们有两个 Monad,那么使用 then,就能够将上一个 Monad和下一个Monad利用函数组合起来将其链接,而不是写为两行语句。

实现以上操做:

public static Maybe<B> Bind<A, B>(this Maybe<A> input, Func<A, Maybe<B>> func)
    => input switch
    {
        { HasValue: true } => func(input.Value),
        _ => Maybe<B>.Nothing()
    };

public static Maybe<B> Then<A, B>(this Maybe<A> input, Maybe<B> next) => next;

完整Maybe<T>实现

public class Maybe<T> where T : notnull
{
    private readonly T innerValue;
    public bool HasValue { get; } = false;
    public T Value => HasValue ? innerValue : throw new InvalidOperationException();

    public Maybe(T value)
    {
        if (value is null) return;
        innerValue = value;
        HasValue = true;
    }

    public Maybe(Maybe<T> value)
    {
        if (!value.HasValue) return;
        innerValue = value.Value;
        HasValue = true;
    }

    private Maybe() { }

    public static implicit operator Maybe<T>(T value) => new Maybe<T>(value);
    public static Maybe<T> Nothing() => new Maybe<T>();
    public override string ToString() => HasValue ? Value.ToString() : "Nothing";
}

public static class MaybeExtensions
{
    public static Maybe<B> Fmap<A, B>(this Maybe<A> input, Func<A, B> func)
        => input switch
        {
            null => Maybe<B>.Nothing(),
            { HasValue: true } => new Maybe<B>(func(input.Value)),
            _ => Maybe<B>.Nothing()
        };

    public static Maybe<B> Apply<A, B>(this Maybe<A> input, Maybe<Func<A, B>> func)
        => (input, func) switch
        {
            _ when input is null || func is null => Maybe<B>.Nothing(),
            ({ HasValue: true }, { HasValue: true }) => new Maybe<B>(func.Value(input.Value)),
            _ => Maybe<B>.Nothing()
        };

    public static Maybe<A> Returns<A>(this A input) => new Maybe<A>(input);

    public static Maybe<B> Bind<A, B>(this Maybe<A> input, Func<A, Maybe<B>> func)
        => input switch
        {
            { HasValue: true } => func(input.Value),
            _ => Maybe<B>.Nothing()
        };

    public static Maybe<B> Then<A, B>(this Maybe<A> input, Maybe<B> next) => next;
}

以上方法能够自行柯里化后使用,以及我调换了一些参数顺序便于使用,因此可能和定义有所出入。

有哪些常见的 Monads

  • Maybe
  • Either
  • Try
  • Reader
  • Writer
  • State
  • IO
  • List
  • ......

C# 中有哪些 Monads

  • Task<T>
  • Nullable<T>
  • IEnumerable<T>+SelectMany
  • ......

为何须要 Monads

想象一下,如今世界上只有一种函数:纯函数。它接收一个参数,而且对于每个参数值,给出固定的返回值,即 f(x)对于相同参数恒不变。

那如今问题来了,若是我须要可空的值 Maybe或者随机数Random等等,前者除了值自己以外,还带有一个是否有值的状态,然后者还跟计算机的运行环境、时间等随机数种子的因素有关。若是咱们全部的函数都是纯函数,那么咱们如何用一个函数去产生 Maybe 和 Random 呢?

前者可能只须要给函数增长一个参数:是否有值,然然后者呢?牵扯到时间、硬件、环境等等一切和产生随机数种子有关的状态,咱们固然能够将全部状态都看成参数传入,而后生成一个随机数,那更复杂的,IO如何处理?

这类函数都是与环境和状态密切相关的,状态是可变的,并不能简单的由参数作映射产生固定的结果,即这类函数具备反作用。可是,咱们能够将状态和值打包起来装在箱子里,这个箱子即 Monad,这样咱们全部涉及到反作用的操做均可以在这个箱子内部完成,将可变的状态隔离在其中,而对外则为一个单体,仍然保持了其不变性。

以随机数 Random为例,咱们想给随机数加 1。(下面的代码我就用 Haskell 放飞自我了)

咱们如今已经有两个函数,nextRandom用于产生一个 Random IntplusOne用于给一个 Int 加 1:

nextRandom :: Random Int // 返回值类型为 Random Int
plusOne :: Int -> Int // 参数类型为 Int,返回值类型为 Int

而后咱们有 bindreturns操做,那咱们只须要利用着两个操做将咱们已有的两个函数组合便可:

bind (nextRandom (returns plusOne))

利用符号表示即为:

nextRandom >>= plusOne

这样咱们将状态等带有反作用的操做所有隔离在了 Monad 中,咱们接触到的东西都是不变的,而且知足 f(g(x)) = g(f(x))

固然这个例子使用Monadbind操做纯属小题大作,此例子中只须要利用Functor的 fmap操做能搞定:

fmap plusOne nextRandom

利用符号表示即为:

plusOne <$> nextRandom
相关文章
相关标签/搜索