函数式编程:声明式与命令式
Functional programming: Declarative vs imperative
函数式编程坚持告诉做什么,而不是如何做。
例如Scala的collections库有filter,map等方法。这些方法使开发者摆脱了传统的for循环,也就是所谓的命令式代码。
但这有什么特别之处呢?
我看到的只是封装在库中各种方法中的for循环相关代码。以命令式范式工作的团队还可以要求其中一名团队成员将所有此类代码封装在一个库中,然后所有其他团队成员都可以使用该库,因此我们摆脱了所有这些命令式代码。这是否意味着团队突然从命令式风格转变为声明式风格?
假设您有一个要在源代码的不同位置执行的算法。您可以一次又一次地实现它,或者编写一个在后台执行它的方法,然后您可以调用它。在我的回答中,我不会关注后者的“特殊”之处,而是关注差异。
当然,如果您一遍又一遍地实施该算法,那么很容易在特定位置应用更改。但问题是您可能需要在某个时候对算法应用特定的更改。如果它在源代码中实现了 1000 次,那么您将需要执行 1000 次更改,然后测试所有更改以确保您没有搞砸。如果这 1000 个更改不完全相同,那么同一算法的不同实现将开始彼此偏离,使下一个这样的更改变得更加困难,因此,随着时间的推移,你在维护这 1000 个地方时会遇到越来越多的问题.
如果您改为实现一个为您执行算法的方法,然后您需要更改算法,您将必须恰好执行一次更改,并且可以减少测试次数,因为 1000 次调用方法将成为算法的用户,而不是实现者,因此负担将集中在一个地方,这将您对算法的关注与其使用分开。
另外,如果你有这样的方法,那么你可以很容易地覆盖它。
示例:
假设您有一个要在集合上实现的循环。通常,循环会遍历每个元素并执行某些操作。
现在,让我们进一步假设您实现了类似于可删除集合的东西,也就是说,集合中的每个元素都有一个 isDeleted 字段或类似的东西。现在,对于这些集合,您希望循环跳过所有已删除的元素。如果您有 1000 个实现循环的地方,您将不得不检查每个地方并查看元素是否可以删除,如果可以,则应用跳过逻辑。它会让你的代码变得多余,更不用说重构时的精神负担和浪费的时间了,因为你需要确定在哪里需要执行更改。然后,如果您犯了一些错误,您将需要修复错误。不熟悉这段代码的人将很难理解它。同时,如果您调用了该循环方法并根据需要执行循环,那么代码将更具可读性、更易于维护并且更不容易出现错误。
所以首先,函数式编程和命令式编程是
等同于黄铜钉,如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
}
在命令式编程中,我必须描述如何做我想做的事情。
这些功能不可互换,因为经过
array 显然与执行 if 语句不同。所以把他们的
字符串的值是完全不同的,即使它们都调用相同的 toString
功能。
但是这两步的形状是完全一样的。我的意思是如果你眯着眼睛
一点点,它们是相同的功能。
他们如何 与循环或 if 语句有关,但 他们所做的 是采用
里面有东西的东西(带整数的数组或带数据的请求)和
把那些东西变成一个字符串,然后 return.
所以也许我们给这些东西起一个更具描述性的名称,这对两者都适用。他们
都是 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)
许多函数式语言可以告诉函数调用哪个版本
type 并且您可以有多个函数定义,每种定义一个。
这样可以更短:
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 是一个 Functor,我可以使用 map 来改变它的内容。我只是说这是一个
适用且无需更多工作,我可以将函数放入其中并调用它
一个功能。我说这是一个半环和繁荣,我可以把树加在一起。和所有
其他已经适用于 Semirings 的东西也适用于我的
树.
函数式编程坚持告诉做什么,而不是如何做。
例如Scala的collections库有filter,map等方法。这些方法使开发者摆脱了传统的for循环,也就是所谓的命令式代码。
但这有什么特别之处呢?
我看到的只是封装在库中各种方法中的for循环相关代码。以命令式范式工作的团队还可以要求其中一名团队成员将所有此类代码封装在一个库中,然后所有其他团队成员都可以使用该库,因此我们摆脱了所有这些命令式代码。这是否意味着团队突然从命令式风格转变为声明式风格?
假设您有一个要在源代码的不同位置执行的算法。您可以一次又一次地实现它,或者编写一个在后台执行它的方法,然后您可以调用它。在我的回答中,我不会关注后者的“特殊”之处,而是关注差异。
当然,如果您一遍又一遍地实施该算法,那么很容易在特定位置应用更改。但问题是您可能需要在某个时候对算法应用特定的更改。如果它在源代码中实现了 1000 次,那么您将需要执行 1000 次更改,然后测试所有更改以确保您没有搞砸。如果这 1000 个更改不完全相同,那么同一算法的不同实现将开始彼此偏离,使下一个这样的更改变得更加困难,因此,随着时间的推移,你在维护这 1000 个地方时会遇到越来越多的问题.
如果您改为实现一个为您执行算法的方法,然后您需要更改算法,您将必须恰好执行一次更改,并且可以减少测试次数,因为 1000 次调用方法将成为算法的用户,而不是实现者,因此负担将集中在一个地方,这将您对算法的关注与其使用分开。
另外,如果你有这样的方法,那么你可以很容易地覆盖它。
示例:
假设您有一个要在集合上实现的循环。通常,循环会遍历每个元素并执行某些操作。
现在,让我们进一步假设您实现了类似于可删除集合的东西,也就是说,集合中的每个元素都有一个 isDeleted 字段或类似的东西。现在,对于这些集合,您希望循环跳过所有已删除的元素。如果您有 1000 个实现循环的地方,您将不得不检查每个地方并查看元素是否可以删除,如果可以,则应用跳过逻辑。它会让你的代码变得多余,更不用说重构时的精神负担和浪费的时间了,因为你需要确定在哪里需要执行更改。然后,如果您犯了一些错误,您将需要修复错误。不熟悉这段代码的人将很难理解它。同时,如果您调用了该循环方法并根据需要执行循环,那么代码将更具可读性、更易于维护并且更不容易出现错误。
所以首先,函数式编程和命令式编程是 等同于黄铜钉,如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
}
在命令式编程中,我必须描述如何做我想做的事情。 这些功能不可互换,因为经过 array 显然与执行 if 语句不同。所以把他们的 字符串的值是完全不同的,即使它们都调用相同的 toString 功能。
但是这两步的形状是完全一样的。我的意思是如果你眯着眼睛 一点点,它们是相同的功能。
他们如何 与循环或 if 语句有关,但 他们所做的 是采用 里面有东西的东西(带整数的数组或带数据的请求)和 把那些东西变成一个字符串,然后 return.
所以也许我们给这些东西起一个更具描述性的名称,这对两者都适用。他们 都是 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)
许多函数式语言可以告诉函数调用哪个版本 type 并且您可以有多个函数定义,每种定义一个。 这样可以更短:
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 是一个 Functor,我可以使用 map 来改变它的内容。我只是说这是一个 适用且无需更多工作,我可以将函数放入其中并调用它 一个功能。我说这是一个半环和繁荣,我可以把树加在一起。和所有 其他已经适用于 Semirings 的东西也适用于我的 树.