前一阵子在写 CPU,导致一直没有什么时间去做其他的事情,现在好不容易做完闲下来了,我又可以水文章了哈哈哈哈哈。

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

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

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

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

Functor

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

现在我们有一个纯函数 IsOdd

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

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

那么现在问题来了,如果我们有一个整数列表,要怎么去做上面这件事情呢?

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

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 = ;
Maybe<int> t2 = Maybe<int>.Nothing();
Func<int, bool> func = x => (x & ) == ;
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 = ;
Maybe<Func<int, bool>> isOdd = new Func<int, bool>(x => (x & ) == ); 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

拿 C# 搞函数式编程 - 2的更多相关文章

  1. 拿 C# 搞函数式编程 - 3

    前言 今天和某个人聊天聊到了 C# 的 LINQ,发现我认识的 LINQ 似乎和大多数人认识的 LINQ 不太一样,怎么个不一样法呢?其实 LINQ 也可以用来搞函数式编程. 当然,并不是说写几个 l ...

  2. 拿 C# 搞函数式编程 - 1

    最近闲下来了,准备出一个 C# 搞 FP 的合集.本合集所有代码均以 C# 8 为示例. 可能你说,为什么要这么做呢?回答:为了好玩.另外,意义党们请 gun cu ke! C# 有委托,而且有 Fu ...

  3. Guava 是个风火轮之函数式编程(3)——表处理

    云栖社区> 博客列表> 正文 Guava 是个风火轮之函数式编程(3)--表处理 潘家邦 2016-01-26 13:19:21 浏览1062 评论0 java Guava 摘要: 早先学 ...

  4. (转) 站在C#和JS的角度细谈函数式编程与闭包

    1.函数式编程是什么? 摘自百度的说法是.函数式编程是种编程典范,它将电脑运算视为函数的计算.函数编程语言最重要的基础是 λ 演算(lambda calculus).而且λ演算的函数可以接受函数当作输 ...

  5. (转)现代C++函数式编程

    本文转自:http://geek.csdn.net/news/detail/96636     现代C++函数式编程 C++ 函数式编程 pipeline 开发经验 柯里化 阅读2127    作者简 ...

  6. Python修饰器的函数式编程

    Python的修饰器的英文名叫Decorator,当你看到这个英文名的时候,你可能会把其跟Design Pattern里的Decorator搞混了,其实这是完全不同的两个东西.虽然好像,他们要干的事都 ...

  7. 测试和恢复性的争论:面向对象vs.函数式编程

    Michael Feathers最近的博文在博客社区引发了一场异常激烈的论战.Feathers发表言论说一些面向对象编程语言的内嵌特性有助于测试的进行,并且使用面向对象编程语言编写的代码更容易恢复. ...

  8. boost 的函数式编程库 Phoenix入门学习

    这篇文章是我学习boost phoenix的总结. 序言 Phoenix是一个C++的函数式编程(function programming)库.Phoenix的函数式编程是构建在函数对象上的.因此,了 ...

  9. 函数式编程很难,这正是你要学习它的原因 | 外刊IT评论网

    函数式编程很难,这正是你要学习它的原因 | 外刊IT评论网 函数式编程很难,这正是你要学习它的原因 156 次分享 新浪微博 腾讯微博 Tweet 人人网 QQ空间 很奇怪不是,很少有人每天都使用函数 ...

随机推荐

  1. Pandas文本数据处理

    先初始化数据 import pandas as pd import numpy as np index = pd.Index(data=["Tom", "Bob" ...

  2. 和manacher有关的乱写

    当初学kmp hash的时候被教导manacher非常的鸡肋 今天因为一篇神奇的题解我忍不住颓废了两节课把它学了 思路,代码都比较好懂 虽然它不如各种自动机霸气,唯一的功能貌似就是$O(n)$求出所有 ...

  3. Spring Cloud Gateway使用简介

    Spring Cloud Gateway是类似Nginx的网关路由代理,有替代原来Spring cloud zuul之意: Spring 5 推出了自己的Spring Cloud Gateway,支持 ...

  4. 在vue中如何使用axios

    1.前言 在Vue1.0的时候有一个官方推荐的 ajax 插件 vue-resource,但是自从 Vue 更新到 2.0 之后,官方就不再更新 vue-resource. 关于为什么放弃推荐? -& ...

  5. P4568 [JLOI2011]飞行路线(分层图)

    机房dalao推荐写的...(标签分层图) 经过前几题的分层图的洗礼,我深刻地体会到了分层图的优点和好处(主要是不想打dp....) 先说题吧.... 很明确,模型是最短路,但是,怎么跑k个,是个问题 ...

  6. Redis持久化的几种方式——深入解析RDB

    Redis 的读写都是在内存中,所以它的性能较高,但在内存中的数据会随着服务器的重启而丢失,为了保证数据不丢失,我们需要将内存中的数据存储到磁盘,以便 Redis 重启时能够从磁盘中恢复原有的数据,而 ...

  7. [复现论文程序图]High Speed Continuous Variable Source-Independent Quantum Random Number Generation

    这次的任务是复现该文章的图2(C),过程如下. ①翻译了整篇文章,断断续续,花了3-4天时间. ②阅读文章,并且记录下每个符号的意义,记在单独的1个word文档里. ③开始编程,用的matlab201 ...

  8. java编程思想第四版第八章习题

    第一题 package net.mindview.polymorphism; //基类-自行车 class Cycle{ } //子类-单轮车 class Unicycle extends Cycle ...

  9. nyoj 86-找球号(一)二分法

    86-找球号(一) 内存限制:64MB 时间限制:3000ms 特判: No 通过数:14 提交数:48 难度:3 题目描述: 在某一国度里流行着一种游戏.游戏规则为:在一堆球中,每个球上都有一个整数 ...

  10. 在开发框架中扩展微软企业库,支持使用ODP.NET(Oracle.ManagedDataAccess.dll)访问Oracle数据库

    在前面随笔<在代码生成工具Database2Sharp中使用ODP.NET(Oracle.ManagedDataAccess.dll)访问Oracle数据库,实现免安装Oracle客户端,兼容3 ...