函数式编程坚持告诉你该做什么,而不是如何做。
例如,Scala的集合库有filter、map等方法。这些方法使开发人员能够摆脱传统的For循环,从而摆脱所谓的命令式代码。
但它有什么特别之处呢?
我看到的只是一个与for循环相关的代码,这些代码封装在库中的各种方法中。在命令式范式中工作的团队还可以要求其团队成员之一将所有此类代码封装在库中,然后所有其他团队成员都可以使用该库,这样我们就可以去掉所有这些命令式代码。这是否意味着团队突然从命令式风格转变为声明式风格?
因此,首先,函数式编程和命令式编程正如图灵教堂所示定理。任何一个人能做的事,另一个人也能做。所以当我我真的更喜欢函数式语言,我不能让电脑做任何事情不能用命令式语言完成。
你将能够找到关于这种区别的各种形式的理论用谷歌快速搜索,所以我会跳过它,试着说明我喜欢什么使用一些伪代码。
例如,假设我有一个整数数组:
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语句有关,但他们所做的是take一个有东西的东西(要么是带int的数组,要么是带数据的请求)把那些东西变成一根绳子,然后回来。
所以,也许我们给这些东西起一个更具描述性的名字,这对两者都适用。他们都是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
}
现在,每种类型的函数都会跟踪如何改变他们的内部,但不是什么。如果我想要一个转换数组的函数或者请求int到字符串,我可以说我想要的:
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它是什么形状的,我可以使用成千上万的函数已经存在的。也许我想实现一种新的树,我只是说树是一个函数,我可以使用map来更改它的内容。我只是说这是一个具有应用性,无需更多工作,我可以将函数放入其中,并像这样调用它函数。我说这是一个半环和繁荣,我可以把树加在一起。以及所有其他已经适用于半环的东西只适用于我的树
假设您有一个算法,要在源代码的各个位置执行。你可以一次又一次地实现它,或者写一个在引擎盖下实现它的方法,你可以调用它;特别的";在后者中,在我的回答中,我将侧重于差异。
当然,如果你一次又一次地实现该算法,那么在特定的地方应用更改就很容易了。但问题是,您可能需要在某个时刻对算法进行特定的更改。如果它在源代码中实现了1000次,那么您需要执行1000次更改,然后测试所有更改,以确保您没有搞砸。如果这1000个更改不完全相同,那么同一算法的单独实现将开始相互偏离,使下一个这样的更改更加困难,因此,随着时间的推移,在维护这1000个位置时,您将遇到越来越多的问题。
如果你实现了一个为你做算法的方法,然后你需要改变算法,你必须只实现一次改变,这样你就可以减少测试的次数,因为对该方法的1000个调用将成为算法的用户,而不是实现者,所以负担将集中在一个地方,这将您对算法的关注与其用途区分开来。
此外,如果你有这样一个方法,那么你可以很容易地覆盖它。
示例:
让我们假设您有一个循环要在集合上实现。通常,循环遍历每个元素并执行某些操作。
现在,让我们进一步假设您实现了类似于可删除集合的东西,也就是说,集合中的每个元素都有一个isDeleted字段或类似的东西。现在,对于这些集合,您希望循环跳过所有已删除的元素。如果您有1000个实现循环的地方,那么您必须查看每个地方,看看元素是否可以删除,如果可以,则应用跳过逻辑。这将使代码变得多余,更不用说重构时的心理负担和浪费时间了,因为您需要确定需要在哪里执行更改。然后,如果你犯了一些错误,你就会有错误需要修复。不熟悉这段代码的人很难理解它。但是,如果你调用了那个循环方法,并根据需要进行循环,那么代码将更可读,更容易维护,也不容易出现错误。