拿 C# 搞函数式编程 - 2

前一阵子在写 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 & 1) == 1;

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/312737.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

520 钻石争霸赛 7-5 大勾股定理 (数学)

基本思路&#xff1a; 这道题暴力拿到14分并不难&#xff0c;根据题意模拟即可&#xff0c;具体代码在下面。 至于最后一个测试点超时的问题&#xff0c;现已解决&#xff0c;AC代码在第二部分哦~ 参考代码&#xff08;14分&#xff09;&#xff1a; #include<bits/stdc.h…

.NETer,如何用.NET Core 3.0武装自己?这样学效率提高10倍!

都2020了 你还不会.NET Core&#xff1f; 2019年&#xff0c;.NET Core 3.0横空出世&#xff0c;越来越多的开发者开始关注.NET Core&#xff0c;越来越多的互联网软件公司开始使用.NET Core&#xff0c;各大.NET招聘岗位要求中&#xff0c;也将.NET Core列为必备技能&#xff…

DataFrame的多dtype创建方法

在创建DataFrame的时候&#xff0c;只有有一个dtype类型。 若使用numpy数组的字典&#xff0c;就可以分别设置dtype类型了。 import numpy as np import pandas as pddata {Site:np.array([Google, Runoob, Wiki],dtypestr),Age:np.array([10, 12, 13], dtypefloat),Year:np.…

ASP.NET Core on K8S深入学习(10)K8S包管理器Helm-Part 2

本篇已加入《.NET Core on K8S学习实践系列文章索引》&#xff0c;可以点击查看更多容器化技术相关系列文章。上一篇 Part 1 中介绍了Helm的基本概念与基本使用&#xff0c;这一篇我们来自定义一个Chart玩玩。自定义一个Chart1 创建Chart首先&#xff0c;通过以下命令创建一个c…

使用Vistual Studio N年,推荐2个异常捕获的技巧

点击上方“dotNET全栈开发”&#xff0c;“设为星标”加“星标★”&#xff0c;每天11.50&#xff0c;好文必达全文约1600字&#xff0c;预计阅读时间3分钟这个n到底是多少年&#xff1f;宇宙第一开发IDE Visual Studio的调试功能非常强大&#xff0c;平常工作debug帮助我们解决…

LeetCode动态规划 斐波那契数

斐波那契数&#xff0c;通常用 F(n) 表示&#xff0c;形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始&#xff0c;后面的每一项数字都是前面两项数字的和。也就是&#xff1a; F(0) 0&#xff0c;F(1) 1 F(n) F(n - 1) F(n - 2)&#xff0c;其中 n > 1 给你 n &a…

Magicodes.Sms短信库的封装和集成

简介Magicodes.Sms是心莱团队封装的短信服务库&#xff0c;已提供Abp模块的封装。Nuget新的包开始使用如果使用Abp相关模块&#xff0c;则使用起来比较简单&#xff0c;具体您可以参考相关单元测试的编写。主要有以下步骤&#xff1a;1.引用对应的Nuget包如&#xff1a;2.添加模…

LeetCode动态规划 使用最小花费爬楼梯

数组的每个下标作为一个阶梯&#xff0c;第 i 个阶梯对应着一个非负数的体力花费值 cost[i]&#xff08;下标从 0 开始&#xff09;。 每当你爬上一个阶梯你都要花费对应的体力值&#xff0c;一旦支付了相应的体力值&#xff0c;你就可以选择向上爬一个阶梯或者爬两个阶梯。 请…

Magicodes.IE编写多框架版本支持和执行单元测试

背景很多情况下&#xff0c;我们编写了一些工具库之后&#xff0c;往往在某些框架版本中会出现一些问题&#xff0c;比如本人最近写的一个导入导出的工具库Magicodes.IE就出现了以下问题&#xff1a;&#xff08;GitHub&#xff1a;https://github.com/xin-lai/Magicodes.IE&am…

2019全球AI训练营五地再同发

不觉已过小大寒&#xff0c;虽然天气渐冷&#xff0c;但我们学习的热情却愈加高涨。因为由MVP发起主办的、2019年的全球AI训练营又要在北京、上海、广州、杭州、宁波五个地方同时举办了&#xff01;去年取得巨大成功的全球活动&#xff0c;今年参与举办的国家地区/城市更是高达…

NLog自定义Layout Renderer

更多精彩内容请关注我们长话短说前文《解剖HttpClientFactory&#xff0c;自由扩展HttpMessageHandler》主要讲如何为HttpClientFactory自定义HttpMessageHandler组件, 现在完成课后的小作业&#xff1a; 将重点日志字段显示到Nlog的Layout Renderer上本文自定义一个NLog Layo…

LeetCode动态规划 跳跃游戏II

给你一个非负整数数组 nums &#xff0c;你最初位于数组的第一个位置。 数组中的每个元素代表你在该位置可以跳跃的最大长度。 你的目标是使用最少的跳跃次数到达数组的最后一个位置。 假设你总是可以到达数组的最后一个位置。 这道题用动态规划做&#xff0c;时间复杂度很高 …

一文了解Nuget的使用

Nuget介绍官网定义&#xff1a;NuGet是.NET的软件包管理器&#xff08;免费&#xff09;。NuGet客户端工具提供了生成和使用软件包的能力。NuGet Gallery 是所有软件包作者和消费者都使用的中央软件包存储库。简单来说&#xff0c;nuget包解决了dll引用的问题&#xff0c;开发者…

银行家算法 C++实现

操作系统模拟之银行家算法。 文件共4份&#xff0c;其中1份cpp&#xff0c;3份.h&#xff0c;代码如下&#xff1a; main.cpp #include <iostream> #include <stdlib.h> #include "initialize.h" #include "check.h" #include "apply.h…

.NET 应用程序支持直接调用 WebAssembly 模块

WebAssembly Runtime 现已添加 .NET Core API&#xff0c;开发者可直接在 .NET 应用程序中调用 WebAssembly 模块。Mozilla 宣布由 Bytecode Alliance 创建的 Wasmtime&#xff08;WebAssembly runtime&#xff09;现已添加处于早期预览版状态的 .NET Core API&#xff0c;这就…

进程调度算法 C++实现

操作系统模拟之进程调度算法。 文件共2份&#xff0c;其中1份cpp&#xff0c;1份.h&#xff0c;代码如下&#xff1a; main.cpp #include "init.h"int main() {printf("欢迎进入演示系统&#xff01;\n");printf("\n现在需要对进程数据初始化\n&quo…

《RPA、AI、.NET Core 与未来》-中国.NET开发者峰会

未 来第四次工业革命&#xff0c;催生了数字自动化劳动&#xff0c;RPA 与 AI 技术的融合&#xff0c;成为智能信息社会的重要环节。当下RPA平台主要采用.NET Framework框架&#xff0c;也限制了 RPA 只用于 Windows 平台。.NET Core 的开放与跨平台特性赋予RPA更大发展空间。…

地址转换算法 C++实现

操作系统模拟之地址转换算法。 文件共3份&#xff0c;其中1份cpp&#xff0c;2份.h&#xff0c;代码如下&#xff1a; main.cpp #include "init.h"int main() {printf("欢迎进入演示系统&#xff01;\n");printf("\n现在需要对进程数据初始化\n&quo…

浅议gRPC的数据传输机制和回调机制

本文来自DotNET技术圈作者&#xff1a;溪源一、引子如您所知&#xff0c;gRPC是目前比较常见的rpc框架&#xff0c;可以方便的作为服务与服务之间的通信基础设施&#xff0c;为构建微服务体系提供非常强有力的支持。而基于.NET Core的gRPC.NET 组件截至2019年11月30日的最新版本…

请求分页算法 Python实现

操作系统模拟之请求分页算法。 文件共1份&#xff0c;代码如下&#xff1a; import math import os import random import copydef alo_opt():print("您选择了OPT算法&#xff0c;执行结果如下&#xff1a;")print("访问页面 物理块 缺页中断")temp_que…