编程语言如何改变你的思维方式
这可能是一篇奇怪的文章。
几年前,我看了一部很棒的电影《降临》,它改变了我对语言的看法。不久之后,我第一次接触了 F#,它又改变了我对编程 语言的看法。
在本文中,我将带您了解《降临》中涉及的一些理论的基础知识,然后探讨语言在编程中的作用。在深入探讨 F# 编程语言之前,我们将比较和对比多种语言,看看它们如何影响我们思考软件工程问题的方式。
我告诉过你这可能是一篇奇怪的文章。
本文的前半部分将涵盖多种语言,而后半部分将重点介绍 F# 以及该语言的结构和语法如何影响我们的编程方法。
本文将主要面向那些在函数式编程或 F# 方面经验不足的读者。理想的读者应该具备 C#、Java、JavaScript、TypeScript、Python 或类似语言的使用经验。
语言对思维的影响
让我们先来一段简短的科幻之旅。
注意:本节包含《降临》剧情前一个小时的轻微剧透,其中大部分内容可从电影预告片中收集。
2016年上映的电影《降临》是一部科幻剧情片,由艾米·亚当斯和杰瑞米·雷纳主演。这是一部我很喜欢,但我老婆却睡着了的电影——节奏比较慢,讲述的是一群科学家试图与抵达世界各地的外星人沟通的故事。
《降临》的重点是语言,特别是语言如何影响我们形成思维的方式,甚至感知现实。
科学家们遇到的外星人主要通过表意文字进行交流,这些表意文字包含圆形图案的完整句子,如下图所示:
影片大约 55 分钟处有一个精彩的片段,他们详细讨论了外星语言及其问题,其中包括我们一位主要科学家说的这句话:
“如果你沉浸在一门外语中,你实际上可以重塑你的大脑”
降临,2016
该片段继续讨论了萨丕尔-沃尔夫假说,以及你所说的语言是否可以改变你的思维方式,或者就《降临》中的人物而言,是否可以改变他们对现实的看法。
正如你所料,这部电影很快就进入了令人费解的科幻领域,但如果这是你喜欢的,那么这是一部非常精彩的电影。
虽然我很喜欢这部电影及其概念,但它也让我开始思考编程以及不同的编程语言如何改变我们对应用程序开发的看法。
这个想法一直萦绕在我的心头,随着我对函数式编程的深入研究,这个想法又重新浮现出来。
编程语言对思维的影响
让我们快速浏览几种不同的编程语言。我们将查看每种语言的一些示例代码,并推测它们可能鼓励或阻碍的思维方式。
免责声明:我包含了一些我尚未深入编程的语言的基本信息,以便提供丰富的语法。这些信息将基于我的一些推测,以及我个人对我知识和经验更丰富的语言的亲身体验。
C#
C# 是一种命令式编程语言,旨在支持面向对象编程 (OOP),其语法灵感源自 C++ 和 Java。与 F# 和 Visual Basic 一样,C# 在 .NET 上运行,并且可以与其他 .NET 编程语言交互。
让我们看一下我几年前的一个游戏项目中的一些 C# 代码示例:
这里我们有一个来自游戏上下文的类,其中包含一个 ApplyDamage 方法。代码从上到下流动,在返回表示所造成伤害的数字之前,可能会或可能不会进入 if 语句来进行条件逻辑。
尽管这里的示例很简单,但这种类型的编程对于面向对象语言来说非常典型,并且鼓励以下类型的行为:
- 从个体对象的角度思考系统
- 关注可能需要或不需要执行的 if 语句
- 添加属性、方法和继承来管理事物增长过程中的复杂性
我应该在这里指出,尽管 C# 是作为面向对象的解释型语言构建的,但它正在转变以允许该语言支持像 F# 这样的函数式编程方法。
JavaScript
JavaScript 是一种面向函数的语言,最初是为浏览器构建的。JavaScript 现已发展到支持全栈应用程序和服务器开发。
下面是来自我的另一个业余游戏项目的 JavaScript 代码片段(看出模式了吗?):
此 JavaScript 代码与我们上面的 C# 示例类似,它代表程序将运行的一系列命令。
然而,它与 C# 存在一些显著的差异。
最值得注意的是,JavaScript 是弱类型的。这意味着它的变量没有预先定义的类型,可以重复用于不同类型的对象。对象可以在程序运行时动态添加甚至删除新属性。函数也可以使用任意数量的参数调用,包括少于或多于函数预期的数量。
因此,JavaScript 程序往往比强类型编程语言感觉更松散一些。这种额外的自由度带来了一些独特的问题,而诸如 rest 和展开运算符 (…) 等附加语法有助于缓解这些问题。
TypeScript
我犹豫是否要包含 TypeScript,因为 TypeScript 只是生成 JavaScript 的另一种方式,但我认为值得使用来自 - 你猜对了 - 我的另一个游戏项目的代码进行简要讨论。
TypeScript 实际上是 JavaScript 加上一些可选的类型声明。TypeScript 编译器接收 TypeScript 代码,并将其编译为浏览器可以理解的 JavaScript 代码。
因此,TypeScript 与 JavaScript 没有太大区别,但它确实添加了类型声明语句。
事实证明,类型声明实际上对你如何思考代码至关重要。现在,你无需费力思考某个变量是什么类型,也不必依赖准确的文档,而是可以在变量或参数旁边看到类型声明。这是一个很好的改进,但它也有一些缺点。
由于 TypeScript 依赖于程序员准确定义类型,因此它会引导你选择易于通过标准语法表示的类型。这反过来又会阻止你将函数作为参数传递。
如果您要声明一个参数,该参数是一个接受数字并返回布尔值的函数,则该语法是可行的,但仍然难以记住、编写和阅读,因此您自然倾向于较少地这样做。
Python
Python 是另一种命令式语言,类似于 C# 或 JavaScript,但它的语法明显不同:
代码示例取自《算法》
作为一个只做过一点 Python 编程的人,我无法过多地谈论 Python 如何改变你的思维方式,只能指出与其他语言相比它的语法有多么优雅。
这是一种非常简约的风格,需要在括号上缩进来定义作用域。这可以减少代码中的噪音,并帮助你专注于逻辑。
根据我的经验,这种极简主义风格使我能够用非常简洁的函数来模拟它,这些函数调用其他函数,从而使代码几乎与 F# 一样紧凑,但可读性更强。
我还应该指出,JavaScript 和 TypeScript 与 Python 的比较:Python 允许你添加类型提示(如上图所示)。不过,这个功能是我上次使用 Python 之后才添加的,因此我无法对此进行过多的讨论。
去
Go(也称为 GoLang)是较新的开发语言,代表另一种静态类型的命令式编程语言。
让我们看看它的语法,因为除了大括号和方括号的位置之外,它在几个方面略有不同:
代码示例取自《算法》
Go 有一些语法上的优点我在其他语言中还没有遇到过。
for 关键字更加灵活,可以根据需要进行无限循环。此外,Go 使用 := 作为declared 和 assignment的简写,从而将噪音降至最低。最后,我们来看看使用 , 和 = 运算符快速交换两个值的方法。
我无法详细阐述 Go 如何影响我们对编写应用程序的看法,但我很好奇那些使用过 Go 的人的经历。
我最初的反应是,它吸收了 Python 的许多优雅之处,并将其应用于类似于 C#、Java 和 JavaScript 的语法,我很好奇这种额外的“语法糖”在改变你对解决问题的看法方面能起到多大作用。
F#
到目前为止,我介绍的所有内容主要涉及命令式编程语言。相比之下,F# 是一种函数式编程语言,它依赖于完全不同的代码模式:
代码示例取自《算法》
F# 与 C# 类似,是强类型的,这些类型由编译器强制执行。但是,F# 编译器会推断出更多类型信息,因此实际的类型关键字很少需要。因此,F# 语法能够更无缝地处理复杂类型,因为在语法中表示这些类型的需要显著减少。
F# 语法通常处理从一个函数到另一个函数的“管道”以产生预期的结果。
在下一节中,我们将更深入地研究 F#,并研究一些涉及管道的情况,然后探索其不同语法可能对您如何思考代码中的问题解决方式产生更广泛的影响。
注意:F# 不是唯一的现代函数式编程语言,但由于我对 F# 语言的熟悉以及本文在技术上属于社区F# advent 2020 系列的一部分,我们将在本文的其余部分集中讨论 F#。
其他语言
如果您想进一步了解与许多当前使用的编程语言相关的语法,我建议您查看GitHub 上的算法,其中包含一些其他函数式编程语言的开源示例集。
我也很想知道您对编程语言如何影响您对编程的看法,所以也请留下评论。
F# 如何改变我对编程的看法
除了对语言的概述之外,让我们更深入地了解 F# 语言以及它的功能如何改变我们处理代码的方式。
F# 不是 C#
我第一次尝试学习 F# 时,感觉非常艰难。这是我学习的第一门函数式编程语言,诸如 monad、函数偏应用、高阶函数之类的术语让我的学习之路充满挑战。
除了这个负担之外,当我学习 F# 时,我专注于“如何在 F# 中编写此 C# 代码?”因此,我在 F# 上的第一次尝试是尝试创建具有可变状态的强类型,就像在 C# 程序中所做的那样。
虽然 F#可以做到这一点,但这并不是 F# 的真正目的。F# 的重点不在于创建具有可变状态和传统成员的类,而在于编写函数,并将这些函数组合成更大问题的解决方案。
在面向对象语言中,我们专注于将逻辑封装在内聚的对象中,并按照特定的顺序调用这些对象的方法。换句话说,类是整个程序的核心,它们的方法负责程序的运行。
F# 鼓励将重点放在单个函数上,并推动您实现不可变状态和将函数链接在一起。函数是主要的吸引力,而支持它们的类型则尽可能精简(尽管 F# 的类型系统非常强大且灵活)。
从管道角度思考
当我想到 F# 应用程序时,我会想象一系列相互连接的管道——每个管道都经过精心设计,确保在给定特定输入的情况下能够产生精确的输出,并且不会产生任何其他副作用。这种类型的函数通常被称为纯函数。
单个管道(或功能)很小且高度专业化,但它们组合在一起却构成了一个功能非常强大的应用程序。
F# 作为一种语言,非常擅长将事物链接在一起,这要归功于它的专门运算符,例如 |> 或“管道”运算符,它采用左侧的值并将其作为右侧函数的最后一个参数。
只需看一下 F# 中这个独立运算符的存在,就可以发现在 F# 中许多事情变得容易,而在 C# 等语言中则比较困难。
例如,看看下面的 F# 代码:
watermelon |> SliceOpen |> RemoveSeeds
这将获取西瓜并将其作为参数传递给函数SliceOpen
,然后获取该调用的结果并将其传递给RemoveSeeds
函数。
我们可以在 F# 中进一步简化这一过程,通过使用运算符从和函数>>
组成一个函数,如下所示:SliceAndRemoveSeeds
SliceOpen
RemoveSeeds
let SliceAndRemoveSeeds = SliceOpen >> RemoveSeeds
watermelon |> SliceAndRemoveSeeds
如果我们想在 C# 代码中做同样的事情,我们必须编写以下内容:
var slicedWatermelon = SliceOpen(watermelon);
var preppedWatermelon = RemoveSeeds(slicedWatermelon);
诚然,我们可以通过以下方式稍微简化一下:
var preppedWatermelon = RemoveSeeds(SliceOpen(watermelon));
这确实有效,但你可能会开始感觉到这门语言有点难以理解,现在你需要学习如何从内到外地阅读你的代码。F# 代码更注重顺序性和极简语法(尽管需要一些时间来适应),因此这种函数式链接代码在 F# 中比在 C# 中更自然。
参数顺序
因为管道运算符总是将结果作为最后一个参数传递给它所管道化的函数,所以我们现在需要开始注意声明参数的顺序。
管道运算符在这方面并不是唯一的,因为诸如函数的部分应用之类的东西也会对参数的排序方式产生影响:
let AddNumbers x y = x + y
let AddTwo = AddNumbers 2
let five = AddTwo 3
这里我们定义了AddTwo
一个函数,它的AddNumbers
第一个参数为 2,第二个参数未指定。接下来,我们可以将AddTwo
第二个参数设为 3,最终将 2 和 3 相加。
由此产生的效果是一种有点新奇和不寻常的感觉,需要考虑你的参数并了解哪一个应该是最后一个。
代码顺序
F# 编译器的智能是有代价的:在 F# 中,您不仅需要注意在文件中首先定义哪个函数才能进行编译,还需要为项目内的各个文件明确设置编译顺序。
例如,在下图中,Gasses.fs
文件可以引用Utils.fs
和中的任何内容Positions.fs
,但不能引用GameObjects.fs
或 下面的任何东西。
这种需要考虑文件和函数顺序的过程乍一看似乎很烦人。然而,它也带来了一些令人欣慰的副作用。
由于函数现在是顺序相关的,因此不可能出现函数 A 调用函数 B,而函数 B 又调用函数 A 的无限循环,模块依赖关系现在也更容易直观地展现。编译器有效地确保了依赖关系只沿特定方向流动。诸如NDepend之类的工具就是围绕这一理念在其他代码库中得以实践而构建的。
智能打字
然而,我上面描述的所有这些功能都只是 F# 真正优势的语法糖。
F# 提供了一些非常有趣的类型声明机制。以下是我的一个业余项目中的 GameObjectType 定义:
这类似于C# 枚举,但提供了一些额外的逻辑。在这里,游戏对象可以是任意数量的事物,但有些事物会关联额外的数据。例如,空气管道需要跟踪流经其中的气体混合物;在 2D 世界中,门要么从左到右,要么从上到下,并且会处于打开或关闭状态。
在面向对象语言中,我们可能会使用继承并为这些门和管道定义子类,但在很多情况下,这可能有点过度——尤其是当我们希望将重点放在简洁、可重用的功能而不是强大的对象上时。
然后我们可以match
脱离 F# 中的对象类型:
在这种情况下,F# 的语法使我们不再使用继承,而是使用智能匹配关键字和表达式来处理各种情况(其中 _ 是通配符,表示任何其他不匹配的情况)。
因此,作为开发人员,我们的重点仍然放在函数上,而不是创建强大的类型。这反过来又使我们更专注于函数如何组合在一起。
空值选项
众所周知,Null 被称为“十亿美元的错误”。虽然我不确定自己是否真的了解这个,但还是有必要向那些不熟悉的人指出 F# 对 Null 的处理方式。
下面是一个简短的 F# 函数,它尝试在另一个 Tile 层中的特定位置查找一个 2D 游戏 Tile。它要么找到 Tile,要么找不到。如果找到了,就需要将该 Tile 合并到当前 Tile 之上:
F# 主要使用option类型来处理空值。一个 option 要么是某个值,要么是none。在 C# 中,Option 可以被视为泛型类型,它本身永远不会为空。
F# 编译器要求你使用特定的方式从选项中获取值,并明确要求你处理选项为none的可能性。这迫使你(作为开发人员)明确处理这些情况,并降低你意外忘记处理 null 的几率。
一般来说,F# 会鼓励你使用空列表或选项,而不是显式地使用空值,而编译器会给你当头一棒,提醒你可能已经忘记的事情。并非所有人都喜欢强大而固执己见的编译器(相信我,我在 JavaScript 世界中倡导 TypeScript 就是这么想的),但 F# 的编译器能够捕捉到我甚至都不知道的错误,这对我作为一名开发人员来说非常有价值。
值得注意的是,其他语言也开始采用这些方法,最显著的是C# 添加了对显式空检查的支持作为一项可选功能。
F# 与乔治·奥威尔的《1984》有何关系
在结束之前,让我们简单回顾一下另一部小说及其对语言的思考:乔治·奥威尔的经典小说《1984》。
我年轻时读过这本书好几遍,最引人注目的一点就是它的语言,以及书中“真理部”如何修改语言,使不良概念更难表达。例如,“坏”之类的词就被“不好”之类的词取代了。
这是一幅引人注目的图片和例子,无论你是否兴奋地在学校阅读《1984》,它都代表了我对 F# 语言的看法。
对我来说,F# 编程语言的主要优点在于F# 使无效状态更难表示。
在编程中,每个决策——包括语言设计者所做的决策——都伴随着权衡。我们倾向于做容易的事情,而那些难以表达的事情,我们则倾向于避免。
F# 的函数式语法学习起来难度很高,几乎需要重新学习不同环境下的编程,但这也使得无效状态在编译器层面的表示更加困难。这个代价是否值得,取决于您和您的团队。
此外,并非所有问题都应该用函数式编程语言来解决。大多数编程工作都是维护现有代码,并非所有代码都具备需要使用函数式编程语言的复杂性。有些问题领域还包含许多真正属于类型的独特信息和逻辑。
结束语
无论您选择在项目中使用哪种编程语言,它都应该满足您的目的,并且应该是您有意识的决定。
当你开始一个新项目时,问自己以下问题:
- 这种语言能让我做什么变得容易?
- 这种语言使什么变得更加困难?
- 寻找或培训能够编写和维护这种语言的开发人员有多难?
- 我的团队需要多少培训才能使用这种语言?
- 未来我能从官方和社区渠道获得多少对这种语言的支持?
- 对于我来说,使用此代码与我们组织使用的其他现有代码协同工作有多难?
- 这种语言能为我提供多少帮助?我自己需要添加什么?
- 我喜欢这种语言鼓励我编写的代码类型吗?
我们需要开始研究语言选择的权衡以及我们的语言将我们推向什么样的模式,因为这些选择可能比你意识到的更重要。
本文是F# Advent 2020系列的一部分。查看该系列,了解更多来自社区成员的 F# 文章。
编程语言如何改变你的思维方式一文首先出现在Kill All Defects上。
鏂囩珷鏉ユ簮锛�https://dev.to/integerman/how-programming-languages-change-how-you-think-ok1