首先提振一下读者的信心。
随着发展,并行计算已经成为主流,函数式编程与并行计算有着天然良好的相性
新世代的语言/框架/库,许多都会采用,或者一定程度上使用函数式编程的思想
即使C#未来大概率不会变成一个完整意义上的支持函数式编程语言,但新世代C#许多的特性都是来自函数式思想。或许很多你认为好用的特性,正是来自于函数式。或许其实你已经深深的陷入了函数式的美妙体验之中
再引用一个我喜欢的up的说法:我还没见到有人深入学习函数式编程后再把它抛弃的,只有还没开始学就抛弃的
“本系列部分内容适合有一定编程基础,有C#或类似语言的编程经验更佳。”
C#部分可能会采用LanguageExt库来进行一部分说明
在一切的开始之前,我们也需要说明,函数式编程绝对不是什么万能的编程模式,但了解函数式编程的思想,绝对是一件好事。对于习惯了面对对象的学习者来说, 函数式的一些概念可能非常反直觉,难以理解。如果此时就望而却步,或是嗤之以鼻的话,你可能就错过了一片未曾开拓过的宝藏区域。现在,暂时放弃以前可能被我们视为准则的,时间/空间复杂度,面向对象,设计模式等许多概念,去了解一个不一样的领域。
再次需要说明的是,面向对象与函数式并不是两个冲突的概念。他们完全可以一起使用,纯粹的OOP和FP都是极端的。
不过,OOP解决问题的方式是存在一定“问题”的,FP会是大多数时候更优秀的方案。在后面的内容,我们会对比两者的实现的区别
无论如何,在使用一门函数式语言后,你一定会以全新的视角去看待以前写的代码,获得不一样的认知。
最后,引用我一个非常喜欢的老师引用过的话
"纸上得来终觉浅,绝知此事要躬行"-
希望读者在后面的实操部分,也能亲自编写属于自己的函数式代码,这样便可以更好的理解函数式魅力。
关于FP各个教程中老生常谈的安全啊或是清晰啊这种。说了对于初学者来说也只是一头雾水。我们且在这里提一句,具体是或不是,等我们学习到了一定程度,自然而然的就会得到自己的答案。
函数式编程,说是模式其实更像是一种戒律,他只是简单的让编程者遵循几个法则,程序自然而然的就会变好了。
在之前的文章中我们可能已经见识到了一大堆概念。但这些都可以理解为,函数式编程的工具。在使用他们之前,我们需要了解函数式编程的核心法则
函数作为第一公民这件事其实也非常重要,即函数与值具有相等的地位,可以作为函数入参,出参,可以被赋值/绑定等操作。不过这个更偏向于概念/语言的先决条件,并且在之前的简介中已经介绍过了,本文中就不再做详细介绍
"变量"不可变法则(不可变性)
这是我们将要学习的第一个法则,在之前我们介绍到了类型不该默认为空时(此处插入文章), 引申思考了“变量”是否应该可变的问题。
我们来简单想一想如果变量(我们不再应该称之为变量,接下来我们将称之为数据)不再可变,我们能获得什么好处
最简单也是最直接的,没错,我们不需要担心这个数据会被意料之外的操作修改。我们可以一直放心的使用他,他会的值从被定义之后就不会再次发生更改。(无需担心篡改)
以此为基础,我们再想的深入一些。如果数据不会被修改,也就是说,我们只能对数据进行读的操作而不是写的操作,显然,并行的读操作直接不会有冲突,不可变的数据天然对并行操作有相性(容易并行)(安全)
这就是最简单,不可变性带给我们的好处。当然此时或许会想,这个不可变好像让人感觉行为受限。 这么想也没错,不过
其一,通过一定的思路转变,我们会在代码中见识到不可变的可能性。
其二,不可变确实也不是绝对的。但与可为空类型一样,不可变应该是默认的行为,可变数据应该要被显示的标记出来,并只在有限的范围内有效(例如性能关键代码等)。
我们来看一个最简单的例子
C#有两种排序的写法 C#为什么又两种排序呢?(因为C#最早是偏向命令式语言)
int[] a = { 5, 3, 1, 4, 6 };
Array.Sort(a);
Console.WriteLine(string.Join(",", a));
//1,3,4,5,6
int[] b = { 5, 3, 1, 4, 6 };
b.Order();
// -> IOrderEnumerable<int>
Console.WriteLine(string.Join(",", b));
// 5,3,1,4,6
可以看到Sort直接改变了原有序列,而Order则是返回了一个新序列
如果我们分别用并行的方式同时排序和求值可以得到如下结果
var c = Enumerable.Range(-100000, 200000)
.Select(s => (long)s)
.ToArray();
Random.Shared.Shuffle(c);
var task = () =>
{
Array.Sort(c);
Console.WriteLine("1:" + c.Sum());
};
var task2 = () =>
{
Console.WriteLine("2:" + c.Sum());
};
Parallel.Invoke(task, task2);
// 2:98528530
// 1:-100000
Random.Shared.Shuffle(c);
var task3 = () =>
{
Console.WriteLine("1:" + c.Order().Sum());
};
var task4 = () =>
{
Console.WriteLine("2:" + c.Sum());
};
Parallel.Invoke(task3, task4);
// 2:-100000
// 1:-100000
可以看到由于Order不会去影响原来的序列,我们同时对原数组同时做类似类似的任意操作不会有负面影响,这就是不变性给我们带来的好处之一。
表达式法则(表达式)
这是我们第二个要记住的法则,在函数式编程中,我们总是倾向于用表达式而不是语句,那么什么是表达式呢,我们给出一个最简单的定义,如果一段代码,他有一个返回值,那么他就是表达式。表达式的主要工作是返回一个值,而语句既然不返回值了,所他总是会执行一些副作用
为什么要采用表达式呢?
其实原因很简单,正是因为他总是有返回值(我们似乎发现函数式编程似乎总是在追求一致性)所以表达式可以做到将返回值输入下一个函数(函数的组合),同时他也更容易消除副作用。
我们还是以C#来举一个例子,
string info = string.Empty;
UserStatus status = UserStatus.Idle;
switch (status)
{
case UserStatus.Idle:
info = "Idle";
break;
case UserStatus.Offline:
info = "Offline";
break;
default:
info = "Unknown";
Console.WriteLine(1); // 副作用
break;
}
我们可以看到我们为了给info准确的值,不得不在switch语句中去修改info的值,其间如果做了一些其他副作用(IO)的事情而不好察觉
而这是我们自C#8引入的switch表达式
string info2 = status switch
{
UserStatus.Idle => "Idle",
UserStatus.Offline => "Offline",
_ => "Unknown"
};
使用switch表达式之时,我们无需再引入任何的副作用。(info是在初始化被赋值而不是后续修改)整体也变得更清晰,更不容易出错。这就是表达式能够带来的好处之一。
再回头看到我们不可变法则中的Linq代码,你会发现Order也是有返回值的,如果我们想进一步对结果操作,可以继续在之后调用扩展函数(函数组合)。表达式总是有值也意味着函数更容易组合使用。
b.Order().Select(s => s * 2).ToArray()
所以在函数式编程中,不希望有所谓的void的存在,void总是不和谐的。事实上C#当中你也不能拿出一个void类型的数据来正常使用。同样是C#中,为什么需要Action和Func两种类型来表达委托,Action的本质其实是Func<Void>,可惜我们做不到这一点。
发现了吗,switch表达式和Linq系列的函数都有浓浓的函数式编程的味道。这正是C#从函数式编程(或是说F#)的思想中吸收过来的功能之一。如果你之前已经喜欢上了这些语法,那么恭喜你,你已经体验到了函数式编程的魅力了
函数式编程不止只有F#这类语言可以使用,C#同样也可以遵循函数式编程的法则,并且会越来越完善。我们将带着函数式编程的武器,继续我们编程的旅程!
最后我们提出一个问题如果一个函数的输入一致时,输出一定一致,且不是返回void的时候,这时候函数变得更接近什么概念了?
标签:Console,函数,C#,编程,序章,我们,表达式 From: https://blog.csdn.net/scixing/article/details/144569395