拿 C# 搞函数式编程 - 2
前一阵子在写 CPU,导致一直没有什么时间去做其他的事情,现在好不容易做完闲下来了,我又可以水文章了哈哈哈哈哈。
有关 FP 的类型部分我打算放到明年再讲,因为现有的 C# 虽然有一个 pattern matching expressions
,但是没有 discriminated unions
和 records
,只能说是个半残废,要实现 FP 那一套的类型异常的复杂。西卡西,discriminated unions
和 records
这两个东西官方已经定到 C# 9 了,所以等明年 C# 9 发布了之后我再继续说这部分的内容。
另外,concepts
(type classes
)、traits
、intersect & sum types
和高阶类型也可能会随着 C# 9、10 一并到来。因此到时候再讲才会讲得更爽。另外吹一波 traits
类型系统,同样是图灵完备的类型系统,在表达力上要比OOP
强太多,欢迎大家入坑,比如 Rust 和未来的 C#。
这一部分我们介绍一下 Functor
、Applicative
和 Monad
都是些什么。
本文试图直观地讲,目的是让读者能比较容易的理解,而不是准确知道其概念如何,因此会尽量避免使用一些专用的术语,如范畴学、数学、λ 计算等等里面的东西。感兴趣的话建议参考其他更专业的资料。
Functor
Functor 也叫做函子。想象一下这样一件事情:
现在我们有一个纯函数 IsOdd
bool IsOdd(int value) => (value & 1) == 1;
这个纯函数只干一件事情:判断输入是不是奇数。
那么现在问题来了,如果我们有一个整数列表,要怎么去做上面这件事情呢?
可能会有人说这太简单了,这样就可:
var list = new List<int>();return list.Select(IsOdd).ToList();
上面这句干了件什么事情呢?其实就是:我们将 IsOdd
函数应用到了列表中的每一个元素上,将产生的新的列表返回。
现在我们做一次抽象,我们将这个列表想象成一个箱子M
,那么我们的需要干的事情就是:把一个装着 A
类型东西的箱子变成一个装着 B
类型东西的箱子(A
、B
类型可相同),即 fmap
函数,而做这个变化的方法就是:进入箱子M
,把里面的A
变成B
。
它分别接收一个把东西从A
变成B
的函数、一个装着A
的M
,产生一个装着B
的M
。
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 Truet2.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
,当 input
和isOdd
任何一个为Nothing
的时候,结果都是Nothing
,否则是Just
,并且将值存到这个 Just
里面。
Monad
Monad 继承自 Applicative,并另外包含几个额外的操作:returns
、bind
和then
。
returns
干的事情和上面的Applicative
中pure
干的事情没有区别。
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 Int
,plusOne
用于给一个 Int
加 1:
nextRandom :: Random Int // 返回值类型为 Random IntplusOne :: Int -> Int // 参数类型为 Int,返回值类型为 Int
然后我们有 bind
和returns
操作,那我们只需要利用着两个操作将我们已有的两个函数组合即可:
bind (nextRandom (returns plusOne))
利用符号表示即为:
nextRandom >>= plusOne
这样我们将状态等带有副作用的操作全部隔离在了 Monad 中,我们接触到的东西都是不变的,并且满足 f(g(x)) = g(f(x))
!
当然这个例子使用Monad
的bind
操作纯属小题大做,此例子中只需要利用Functor
的 fmap
操作能搞定:
fmap plusOne nextRandom
利用符号表示即为:
plusOne <$> nextRandom
拿 C# 搞函数式编程 - 2相关推荐
- 拿 C# 搞函数式编程
最近闲下来了,准备出一个 C# 搞 FP 的合集.本合集所有代码均以 C# 8 为示例. 可能你说,为什么要这么做呢?回答:为了好玩.另外,意义党们请 gun cu ke! C# 有委托,而且有 Fu ...
- 过程或函数的副作用是_Python函数和函数式编程(两万字长文警告!一文彻底搞定函数,建议收藏!)...
Python函数和函数式编程 函数是可重用的程序代码段,在Python中有常用的内置函数,例如len().sum()等. 在Pyhon模块和程序中也可以自定义函数.使用函数可以提高编程效率. 1.函数 ...
- 用通俗易懂的大白话搞明白Java里的函数式编程和Lambda表达式
今天,用通俗易懂的大白话来彻底搞明白Java里的函数式编程和Lambda表达式 为什么引入函数式编程,lambda表达式? 大家都知道,JDK1.8引入了函数式编程,lambda表达式. 那有没有想过 ...
- less 函数_Python中的函数式编程教程,学会用一行代码搞定所有内容
前言 在本文中,您将了解什么是函数范型,以及如何在Python中使用函数式编程.在Python中,函数式编程中的map和filter可以做与列表相同的事情.这打破了Python的禅宗规则之一,因此函数 ...
- 玩转 JavaScript 面试:何为函数式编程?
函数式编程在 JavaScript 领域着实已经成为一个热门话题.就在几年前,很多 JavaScript 程序员甚至都不知道啥是函数式编程,但是就在近三年里我看到过的每一个大型应用的代码库中都包含了函 ...
- 函数式编程学习之路(三)
入门: 函数式编程之艰难,在于这玩意更接近数学,就是数学之"用",大牛们在云端,玩纯数学去了,弄出一堆公式及概念,码农们爬在地上,辛苦耕耘,要的是看得见摸得着的,函数式编程就是要打 ...
- 测试和恢复性的争论:面向对象vs.函数式编程
Michael Feathers最近的博文在博客社区引发了一场异常激烈的论战.Feathers发表言论说一些面向对象编程语言的内嵌特性有助于测试的进行,并且使用面向对象编程语言编写的代码更容易恢复. ...
- 开发日记-20190517 关键词 函数式编程(一)
前言: 这块其实是我对于<Java函数式编程>的读书笔记,有句话其实说的挺不错,每当你需要二刷一本书的时候,说明这本书你白读了.所以我争取一遍搞定它,不做浪费时间的二刷操作. 第一章< ...
- 函数式编程——做到并发,不可变数据修改就只能复制后修改返回
函数式编程 from:https://coolshell.cn/articles/10822.html 当我们说起函数式编程来说,我们会看到如下函数式编程的长相: 函数式编程的三大特性: immuta ...
最新文章
- Bioinformatics | 预测药物相互作用的多模式深度学习框架
- 电脑启动后,没有桌面
- mysql 2进制 安装_mysql二进制安装
- HTTP协议(5)HTTP请求和响应
- 基于JAVA+SpringMVC+Mybatis+MYSQL的仓库管理系统
- Linux操作系统原理与应用02:内存寻址
- alert点击完确定关闭浏览器窗口_为Microsoft Edge浏览器开启标签组功能
- MQTT、CoAP 还是 LwM2M?主流物联网协议如何选择
- NSA 将向公众开源逆向工程工具 GHIDRA
- 菩提心的修法-菩提心的修法
- 短信平台系统 后台功能简介 网页版短信通道介绍 短信平台源代码搭建
- 五个优秀的视频格式转换工具
- 2007第一天上班想哭
- GBase 8c查看数据
- 使用Matlab实现:Jacobi、Gauss-Seidel迭代
- blog群发王(价值1980元)源代码提供 1
- IRremoteESP8266库 红外控制空调方法
- 微软kestrel服务器开启,asp.net-core - 如何在ASP.NET Kestrel服务器中设置TCP待办事项 - SO中文参考 - www.soinside.com...
- 外挂太多怎么解?云上找出路
- UE4 反射系统(UBT和UHT)介绍