为什么在函数式编程方面,TypeScript 是比 JavaScript 更好的选择?
在这篇文章中,我想讨论静态类型在函数式编程语言中的重要性,以及为什么由于 JavaScript 缺乏静态类型系统,TypeScript 在函数式编程方面比 JavaScript 更好。
函数式编程代码库中没有类型的生活
请尝试设想一个假设的情况,以便我们展示静态类型的价值。假设您正在为一个与选举相关的应用程序编写代码。您刚刚加入团队,这个应用程序规模很大。您需要编写一个新功能,其中一项要求是确保应用程序的用户有资格在选举中投票。团队中的一位老成员告诉我们,我们需要的部分代码已经在一个名为 的模块中实现@domain/elections
,我们可以按如下方式导入它:
import { isEligibleToVote } from "@domain/elections";
导入是一个很好的起点,我们非常感谢同事提供的帮助。是时候完成一些工作了。但是,我们遇到了一个问题。我们不知道如何使用isEligibleToVote
。如果我们尝试通过名称猜测的类型isEligibleToVote
,我们可能会认为它很可能是一个函数,但我们不知道应该为其提供什么参数:
isEligibleToVote(????);
我们不怕阅读别人的代码,我们打开模块源代码的源代码@domain/elections
,我们会遇到以下情况:
const either = (f, g) => arg => f(arg) || g(arg);
const both = (f, g) => arg => f(arg) && g(arg);
const OUR_COUNTRY = "Ireland";
const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY;
const wasNaturalized = person => Boolean(person.naturalizationDate);
const isOver18 = person => person.age >= 18;
const isCitizen = either(wasBornInCountry, wasNaturalized);
export const isEligibleToVote = both(isOver18, isCitizen);
上述代码片段采用了函数式编程风格。它isEligibleToVote
执行了一系列检查:
- 该人必须年满 10 岁
- 此人必须是公民
- 要成为公民,该人必须在该国出生或入籍
我们需要在脑子里做一些逆向工程才能解码上面的代码。我几乎确定这isEligibleToVote
是一个函数,但现在我有些怀疑,因为我没有在它的声明中看到function
关键字或箭头函数 ( ):=>
const isEligibleToVote = both(isOver18, isCitizen);
为了弄清楚它是什么,我们需要检查both
函数的功能。我可以看到,它们都接受两个参数f
和,g
并且我可以看到它们是函数,因为它们被调用了f(arg)
和g(arg)
。该both
函数返回一个arg => f(arg) && g(arg)
接受名为的参数的函数args
,其形状目前我们完全不知道:
const both = (f, g) => arg => f(arg) && g(arg);
现在我们可以返回isEligibleToVote
函数并再次尝试检查,看看能否发现一些新的东西。我们现在知道了 是函数isEligibleToVote
返回的函数,我们也知道是,所以它正在执行类似于以下内容的操作:both
arg => f(arg) && g(arg)
f
isOver18
g
isCitizen
isEligibleToVote
const isEligibleToVote = arg => isOver18(arg) && isCitizen(arg);
我们仍然需要找出参数是什么arg
。我们可以检查isOver18
和isCitizen
函数来找到一些细节。
const isOver18 = person => person.age >= 18;
这条信息很有帮助。现在我们知道了isOver18
需要一个名为 的参数person
,并且它是一个带有名为 的属性的对象,age
通过比较,我们还能够猜出person.age >= 18
它age
是一个数字。
让我们看isCitizen
一下该函数:
const isCitizen = either(wasBornInCountry, wasNaturalized);
我们在这里运气不佳,我们需要检查either
、wasBornInCountry
和wasNaturalized
功能:
const either = (f, g) => arg => f(arg) || g(arg);
const OUR_COUNTRY = "Ireland";
const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY;
const wasNaturalized = person => Boolean(person.naturalizationDate);
和都wasBornInCountry
期望wasNaturalized
一个名为的参数person
,现在我们发现了新的属性:
- 该
birthCountry
属性似乎是一个字符串 - 该
naturalizationDate
属性似乎为日期或空
该either
函数将参数传递给 和wasBornInCountry
,wasNaturalized
这意味着arg
必须是一个人。这花费了我们大量的认知精力,我们感到很累,但现在我们知道可以isElegibleToVote
按如下方式使用该函数:
isEligibleToVote({
age: 27,
birthCountry: "Ireland",
naturalizationDate: null
});
我们可以使用 JSDoc 等文档来解决其中的一些问题。然而,这意味着更多的工作,而且文档很快就会过时。
TypeScript 可以帮助验证我们的 JSDoc 注释是否与代码库保持同步。但是,如果我们要这样做,为什么不一开始就采用 TypeScript 呢?
函数式编程代码库中的类型生活
现在我们知道了在没有类型的函数式编程代码库中工作有多么困难,我们将看看在具有静态类型的函数式编程代码库上工作的感觉。我们将回到同样的起点,我们加入了一家公司,我们的一位同事向我们介绍了这个@domain/elections
模块。然而,这一次我们身处一个平行宇宙,代码库是静态类型的。
import { isEligibleToVote } from "@domain/elections";
我们不知道它isEligibleToVote
是不是函数。不过,这次我们可以做的远不止猜测。我们可以使用 IDE 将鼠标悬停在isEligibleToVote
变量上,以确认它是一个函数:
然后我们可以尝试调用该isEligibleToVote
函数,我们的 IDE 会让我们知道需要传递一个类型的对象Person
作为参数:
如果我们尝试传递一个对象文字,我们的 IDE 将显示该类型的所有属性及其Person
类型:
就是这样!无需思考或记录!这一切都归功于 TypeScript 类型系统。
以下代码片段包含模块的类型安全版本@domain/elections
:
interface Person {
birthCountry: string;
naturalizationDate: Date | null;
age: number;
}
const either = <T1>(
f: (a: T1) => boolean,
g: (a: T1) => boolean
) => (arg: T1) => f(arg) || g(arg);
const both = <T1>(
f: (a: T1) => boolean,
g: (a: T1) => boolean
) => (arg: T1) => f(arg) && g(arg);
const OUR_COUNTRY = "Ireland";
const wasBornInCountry = (person: Person) => person.birthCountry === OUR_COUNTRY;
const wasNaturalized = (person: Person) => Boolean(person.naturalizationDate);
const isOver18 = (person: Person) => person.age >= 18;
const isCitizen = either(wasBornInCountry, wasNaturalized);
export const isEligibleToVote = both(isOver18, isCitizen);
添加类型注解可能需要一些额外的类型,但其好处无疑是值得的。我们的代码将更不容易出错,并且能够自文档化,团队成员的工作效率也会更高,因为他们将花费更少的时间来理解已有的代码。
通用的用户体验原则“ Don't Make Me Think”也能极大地提升我们的代码质量。记住,我们花在阅读代码上的时间远比写代码的时间多得多。
关于函数式编程语言中的类型
函数式编程语言不一定是静态类型的。然而,函数式编程语言往往倾向于静态类型。根据维基百科,这种趋势自 20 世纪 70 年代以来就一直盛行:
自 1970 年代 Hindley–Milner 类型推断的出现以来,函数式编程语言倾向于使用类型化的 lambda 演算,在编译时拒绝所有无效程序,并冒着出现假阳性(false positive)的风险。这与 Lisp 及其变体(例如 Scheme)中使用的无类型 lambda 演算相反,后者在编译时接受所有有效程序,并冒着出现假阴性(false negative)的风险。Lisp 及其变体(例如 Scheme)中使用的无类型 lambda演算在运行时拒绝所有无效程序,前提是信息足以保证不会拒绝有效程序。代数数据类型的使用使得复杂数据结构的操作变得方便;强编译时类型检查的存在使得程序在缺乏其他可靠性技术(例如测试驱动开发)的情况下更加可靠,而类型推断使程序员在大多数情况下无需手动向编译器声明类型。
让我们考虑一下没有类型的面向对象isEligibleToVote
特性的实现:
const OUR_COUNTRY = "Ireland";
export class Person {
constructor(birthCountry, age, naturalizationDate) {
this._birthCountry = birthCountry;
this._age = age;
this._naturalizationDate = naturalizationDate;
}
_wasBornInCountry() {
return this._birthCountry === OUR_COUNTRY;
}
_wasNaturalized() {
return Boolean(this._naturalizationDate);
}
_isOver18() {
return this._age >= 18;
}
_isCitizen() {
return this._wasBornInCountry() || this._wasNaturalized();
}
isEligibleToVote() {
return this._isOver18() && this._isCitizen();
}
}
弄清楚如何调用前面的代码并不是一个简单的任务:
import { Person } from "@domain/elections";
new Person("Ireland", 27, null).isEligibleToVote();
再一次,由于没有类型,我们被迫查看实现细节。
constructor(birthCountry, age, naturalizationDate) {
this._birthCountry = birthCountry;
this._age = age;
this._naturalizationDate = naturalizationDate;
}
当我们使用静态类型时事情变得更容易:
const OUR_COUNTRY = "Ireland";
class Person {
private readonly _birthCountry: string;
private readonly _naturalizationDate: Date | null;
private readonly _age: number;
public constructor(
birthCountry: string,
age: number,
naturalizationDate: Date | null
) {
this._birthCountry = birthCountry;
this._age = age;
this._naturalizationDate = naturalizationDate;
}
private _wasBornInCountry() {
return this._birthCountry === OUR_COUNTRY;
}
private _wasNaturalized() {
return Boolean(this._naturalizationDate);
}
private _isOver18() {
return this._age >= 18;
}
private _isCitizen() {
return this._wasBornInCountry() || this._wasNaturalized();
}
public isEligibleToVote() {
return this._isOver18() && this._isCitizen();
}
}
构造函数告诉我们需要多少个参数以及每个参数的预期类型:
public constructor(
birthCountry: string,
age: number,
naturalizationDate: Date | null
) {
this._birthCountry = birthCountry;
this._age = age;
this._naturalizationDate = naturalizationDate;
}
我个人认为函数式编程通常比面向对象编程更难逆向。这或许是因为我的面向对象背景。然而,无论原因如何,我确信一件事:类型确实让我的工作更轻松,而且当我在处理函数式编程代码库时,它们的好处更加明显。
概括
静态类型是宝贵的信息来源。由于我们花在阅读代码上的时间远多于编写代码的时间,我们应该优化工作流程,以便更高效地阅读代码,而不是更高效地编写代码。类型可以帮助我们减少大量的认知投入,从而专注于我们试图解决的业务问题。
虽然所有这些在面向对象编程代码库中都是正确的,但在函数式编程代码库中,其优势更加明显,这正是我认为 TypeScript 在函数式编程方面比 JavaScript 更好的原因。你怎么看?
如果你喜欢这篇文章,并且对函数式编程或 TypeScript 感兴趣,请查看我即将出版的新书《使用 TypeScript 进行函数式编程》。
鏂囩珷鏉由簮锛�https://dev.to/wolksoftware/why-typescript-is-a-better-option-than-javascript-when-it-comes-to- functional-programming-3mp0