函数式编程坚持告诉做什么,而不是怎么做。
例如,Scala 的集合库具有 filter、map 等方法。这些方法使开发人员能够摆脱传统的 for 循环,因此称为命令式代码。
但它有什么特别之处呢?
我所看到的只是一个封装在库中各种方法中的与循环相关的代码。以命令式范式工作的团队也可以要求其团队成员将所有此类代码封装在库中,然后所有其他团队成员都可以使用该库,因此我们摆脱了所有这些命令式代码。这是否意味着团队突然从命令式转变为声明式?
函数式编程坚持告诉做什么,而不是怎么做。
例如,Scala 的集合库具有 filter、map 等方法。这些方法使开发人员能够摆脱传统的 for 循环,因此称为命令式代码。
但它有什么特别之处呢?
我所看到的只是一个封装在库中各种方法中的与循环相关的代码。以命令式范式工作的团队也可以要求其团队成员将所有此类代码封装在库中,然后所有其他团队成员都可以使用该库,因此我们摆脱了所有这些命令式代码。这是否意味着团队突然从命令式转变为声明式?
因此,首先,正如 Church-Turing 定理所示,函数式编程和命令式编程在归结为黄铜大头针时是等价的。一个人能做的事,另一个人也能做。所以虽然我真的更喜欢函数式语言,但我不能让计算机做任何用命令式语言做不到的事情。
通过快速的 google 搜索,您将能够找到有关区别的各种正式理论,因此我将跳过它并尝试使用一些伪代码来说明我喜欢什么。
例如,假设我有一个整数数组:
var arrayOfInts = [1, 2, 3, 4, 5, 6]
我想把它们变成字符串:
function turnsArrayOfNumbersIntoStrings(array) {
var arrayOfStrings = []
for (var i = 0; i < arrayOfInts; i++) {
arrayOfStrings[i] = toString(arrayOfInts[i])
}
return arrayOfStrings
}
稍后,我正在发出网络请求:
var result = getRequest("http://some.api")
这给了我一个数字,我也希望它是一个字符串:
function getDataFromResultAsString(result) {
var returnValue = {success:, data:}
if (result.success) {
returnValue.success = true
returnValue.data = toString(data)
}
return returnValue
}
在命令式编程中,我必须描述如何做我想做的事。这些函数不可互换,因为遍历数组显然与执行 if 语句不同。因此将它们的值转换为字符串是完全不同的,即使它们都调用相同的 toString 函数。
但这两个台阶的形状是完全一样的。我的意思是,如果你稍微眯一下眼,它们的功能是一样的。
他们如何做到这一点与循环或 if 语句有关,但他们所做的是将其中包含内容的东西(带有整数的数组或带有数据的请求)转换为字符串,然后返回。
所以也许我们给这些东西一个更具描述性的名字,这两者都适用。它们都是 ThingWithStuff。即一个数组就是一个ThingWithStuff,一个请求结果就是一个ThingWithStuff。它们每个都有一个函数,通常称为 stuffToString,可以更改里面的东西。
函数式编程的其中一件事是一阶函数:函数可以将函数作为参数。所以我可以用这样的东西使它更通用:
function requestStuffTo(modifier, result) {
var returnValue = {success:, data:}
if (result.success) {
returnValue.success = true
returnValue.data = modifier(data)
}
return returnValue
}
function arrayStuffTo(modifier, array) {
var arrayOfStrings = []
for (var i = 0; i < arrayOfInts; i++) {
arrayOfStrings[i] = modifier(arrayOfInts[i])
}
return arrayOfStrings
}
现在,每种类型的函数都会跟踪如何更改它们的内部结构,但不是什么。如果我想要一个将整数数组或请求转换为字符串的函数,我可以说出我想要的:
arrayStuffTo(toString, array)
requestStuffTo(toString, request)
但我不必说我想要它,因为这是在早期的函数中完成的。后来,当我想要数组和请求时,布尔值:
arrayStuffTo(toBoolean, array)
requestStuffTo(toBoolean, request)
许多函数式语言可以通过类型来判断要调用哪个版本的函数,并且您可以有多个函数定义,每种类型一个。所以这可以更短:
var newArray = stuffTo(toBoolean, array)
var newRequest = stuffTo(toBoolean, request)
我可以对参数进行柯里化,然后部分应用该函数:
function stuffToBoolean = stuffTo(toBoolean)
var newArray = stuffToBoolean(array)
var newRequst = stuffToBoolean(request)
现在他们是一样的!
现在,当我想添加一个新的 ThingWithStuff 类型时,我所要做的就是为那个东西实现 stuffTo 。
function stuffTo(modifier, maybe) {
if (let Just thing = maybe) {
return Just(modifier(thing))
} else {
return Nothing
}
}
现在我可以免费使用我已经拥有的新功能了!
var newMaybe = stuffToBoolean(maybe)
var newMaybe2 = stuffToString(maybe)
或者,我可以添加一个新功能:
function stuffTimesTwo(thing) {
return stuffTo((*)2), thing)
}
而且我已经可以将它与任何东西一起使用!
var newArray = stuffTimesTwo(array)
var newResult = stuffTimesTwo(result)
var newMaybe = stuffTimesTwo(newMaybe)
我什至可以使用一个旧功能并轻松地将其转换为适用于任何 ThingWithStuff 的功能:
function liftToThing(oldFunction, thing) {
return stuffTo(oldFunction, thing)
}
function printThingContents = liftToThing(print)
(ThingWithStuff 通常称为 Functor,而 stuffTo 通常称为 map)
你可以用命令式语言做所有相同的事情,但例如 Haskell 已经有数百种不同的形状事物和数千个对这些事物起作用的函数。所以如果我添加一个新东西,我所要做的就是告诉 Haskell 它是什么形状,我可以使用已经存在的数千个函数。也许我想实现一种新的 Tree,我只是说 Tree 是一个 Functor,我可以使用 map 来改变它的内容。我只是说它是一个 Applicative 并且没有更多的工作,我可以将函数放入其中并像函数一样调用它。我说这是一个半环和繁荣,我可以把树加在一起。所有其他已经适用于 Semirings 的东西都适用于我的 Tree。
假设您有一个算法,您想在源代码的不同位置执行该算法。您可以一次又一次地实现它,或者编写一个在后台执行它的方法,然后您可以调用它。在我的回答中,我将重点关注差异,而不是关注后者的“特殊”。
自然,如果您一遍又一遍地实现该算法,那么在特定位置应用更改很容易。但问题是您可能需要在某个时候对算法应用特定的更改。如果它在源代码中实现了 1000 次,那么您将需要执行 1000 次更改,然后测试所有更改以确保您没有搞砸。如果这 1000 个更改不完全相同,那么同一算法的单独实现将开始相互偏离,使下一次这样的更改更加困难,因此,随着时间的推移,您在维护这 1000 个位置时会遇到越来越多的问题.
如果您实现了一个为您执行算法的方法,然后您需要更改算法,您将必须只执行一次更改,您可以减少测试次数,因为对该方法的 1000 次调用将成为算法,而不是实现者,因此负担将集中在一个地方,这将您对算法的关注与其用途分开。
此外,如果您有这样的方法,那么您可以轻松地覆盖它。
例子:
假设您有一个要在集合上实现的循环。通常,循环遍历每个元素并执行某些操作。
现在,让我们进一步假设您实现了类似可删除集合的东西,也就是说,集合中的每个元素都有一个 isDeleted 字段或类似的东西。现在,对于这些集合,您希望循环跳过所有已删除的元素。如果您有 1000 个实现循环的位置,则必须查看每个位置并查看元素是否可以删除,如果可以,应用跳过逻辑。它会让你的代码变得多余,更不用说在你进行重构时的精神负担和浪费的时间,因为你需要确定需要在哪里进行更改。然后,如果您犯了一些错误,您将有错误需要修复。不熟悉此代码的人将很难理解它。同时,如果您调用了该循环方法并根据需要执行循环,