嵌套函数的用途或优点是什么?
What are the uses or advantages of nested functions?
我最近一直在研究一个数字系统转换器,发现了这个嵌套函数代码块,这让我对它的使用产生了疑问。
就我而言,第一个代码输出与第二个代码相同的结果。那么为什么有人会求助于更复杂的东西呢?这种方法有什么优点?
convertBase(num).numFrom(from).numTo(to);
let convertBase = (num) => {
return {
numFrom: function (baseFrom) {
return {
numTo: function (baseTo) {
}
}
}
}
}
convertBase(num, from, to);
let convertBase = (num, baseFrom, baseTo) => {
return parseInt(num, baseFrom).toString(baseTo);
}
它提供了一个 fluent interface 来明确哪个值去哪里。
convert(3).fromBase(16).toBase(2);
严格来说比
更好(更易维护、更易读、更不容易出错)
convertBase(3, 16, 2);
其中 3 个整数参数的顺序不明显。
从另一个函数返回一个函数的基本概念称为闭包。
闭包的概念可以应用于部分应用和柯里化。
你可以阅读它们 here
它有适当的例子说明为什么嵌套函数更好。
这与其说是嵌套函数,不如说是 schönfinkeling / currying。 Schönfinkeling / currying 以开发此技术的 Moses Schönfinkel(在 Gottlob Frege 之前介绍它之后)和完善并描述它的 Haskell Curry 的名字命名。
简单来说,柯里化是一种技术,可以将任何 n 个参数的函数转换为 n-1 个参数的函数其中 returns 一个接受 nth 参数的函数。通过反复应用这一点,您可以证明您永远不需要具有多个参数的函数来为具有任意多个参数的函数建模。
这是一个例子。我可以转换一个将两个数字相加的函数:
function add(a, b) { return a + b; }
add(2, 3)
//=> 5
进入 "adder factory" 那个 returns 加法器函数,当调用它时将产生两个数字的总和:
function adderFactory(a) {
return function adder(b) { return a + b; };
}
const twoAdder = adderFactory(2);
twoAdder(3)
//=> 5
或
adderFactory(2)(3)
//=> 5
现在,您可能会想:"but ECMAScript supports functions with more than one argument, so why would I simulate them using currying, if I can have them natively?"您是对的!出于这个原因使用柯里化是没有意义的。
但是,您可能还想用函数做另一件有趣的事情:部分应用。 "Function application" 只是函数式编程代表 "calling a function",所以 "partial application" 意味着 "calling a function with only a subset of its arguments"。部分应用仅使用其部分参数调用一个函数,并生成一个仅针对这些参数 specialized 的函数。在支持部分应用的语言中,我可以这样做:
const fourAdder = add(4, ?);
但是,ECMAScript 没有部分应用。
然而,当我柯里化我的函数时,我可以做 "sort-of partial application",我至少可以只提供前几个参数,而忽略最后几个参数。这意味着你必须考虑哪些参数更可能是固定的,哪些参数更可能是可变的,并且你应该按 "variability".
对它们进行排序
因此,对于您发布的函数,可以创建一个只能将一个特定数字从一个特定基数转换为任意数量基数的基数转换器。我必须承认,这实际上并不是很有用。如果像这样定义函数会更有用:
const convertFromBaseToBase = baseFrom =>
baseTo =>
num => parseInt(num, baseFrom).toString(baseTo);
convertFromBaseToBase(2)(8)('1001')
//=> '11'
现在,例如,您可以像这样创建一个从八进制到十六进制的转换器:
const octalToHexadecimalConverter = convertFromBaseToBase(8)(16);
octalToHexadecimalConverter('17')
//=> "F"
注意!由于你只能 "partially apply from the right" 的限制,你实际上也可以使用带有默认参数的可选参数来做到这一点,有点像这样:
const baseToToken = Symbol('baseTo'),
numToken = Symbol('num');
function convertFromBaseToBase(baseFrom, baseTo=baseToToken, num=numToken) {
if (num === numToken) {
if (baseTo === baseToToken) {
return (baseTo, num=numToken) =>
num === numToken ?
num => parseInt(num, baseFrom).toString(baseTo) :
parseInt(num, baseFrom).toString(baseTo);
} else {
return num => parseInt(num, baseFrom).toString(baseTo);
}
} else {
return parseInt(num, baseFrom).toString(baseTo);
}
}
convertFromBaseToBase(8, 16, '17')
//=> 'F'
convertFromBaseToBase(8, 16)('17')
//=> 'F'
convertFromBaseToBase(8)(16)('17')
//=> 'F'
convertFromBaseToBase(8)(16, '17')
//=> 'F'
但是,如您所见,这开始变得非常丑陋,非常快。
问题中的代码片段还有一个有用的原因:它提供了一个 流畅的界面 为特定参数命名,这样您就不会混淆这两个数字参数baseFrom
和 baseTo
。然而,这也可以通过其他几种方式解决。一种是通过命名函数,以便清楚 baseFrom
或 baseTo
是第一个,即 convertBase(num, baseFrom, baseTo)
而不是 convertNumberFromBaseToBase(num, baseFrom, baseTo)
。另一种可能性是使用对象参数,如下所示:
function convertBase({ num, baseFrom, baseTo }) {
return parseInt(num, baseFrom).toString(baseTo);
}
convertBase({ num: '17', baseFrom: 8, baseTo: 16 })
//=> 'F'
但是,即使使用更具描述性的函数名称或流畅的界面,更改参数的顺序仍然有意义,以使柯里化和部分应用更有用。
另请注意,我根本没有说任何关于 不是 用于柯里化的嵌套函数,例如在本例中 [代码改编自 Ruby Recursive Indexing/Searching Method (Using Middle Comparison) Returning Incorrect Index Value]:
function bsearch(arr, target) {
function bsearchRec(arr, target, offset=0) {
const middleIndex = Math.floor(arr.length / 2);
if (arr[middleIndex] === target) { return offset + middleIndex; }
if (arr.length === 1) { return undefined; }
if (target > arr[middleIndex]) {
return bsearchRec(arr.slice(middleIndex+1), target, offset + middleIndex + 1);
} else if (target < arr[middleIndex]) {
return bsearchRec(arr.slice(0, middleIndex), target, offset);
}
}
return bsearchRec(arr, target);
}
bsearch([1, 3, 4, 5, 9], 5)
//=> 3
这里,嵌套函数bsearchRec
嵌套在bsearch
内部,因为它是bsearch
的私有内部实现细节,除了bsearch
的作者之外,没有人应该知道了。
最后,函数是 ECMAScript 中用于封装的载体。特别是,函数是 ECMAScript 实现对象的方式。对象具有由名称和封装标识的行为。在大多数 OO 语言中,行为、封装和名称到行为的映射(又名 "method calls")这三样东西由一个实体(对象)提供。在 ECMAScript 中,封装由函数(闭包)提供,行为由函数提供(嵌套在闭包内以共享私有状态),从名称到行为的映射由字典提供,它们被混淆地称为 objects,尽管它们只实现了对象的三分之一。
所以,如果没有嵌套函数,ECMAScript 中就没有封装,最重要的是,没有对象!甚至模块和 类 也主要是嵌套函数之上的语法糖。
我最近一直在研究一个数字系统转换器,发现了这个嵌套函数代码块,这让我对它的使用产生了疑问。
就我而言,第一个代码输出与第二个代码相同的结果。那么为什么有人会求助于更复杂的东西呢?这种方法有什么优点?
convertBase(num).numFrom(from).numTo(to);
let convertBase = (num) => {
return {
numFrom: function (baseFrom) {
return {
numTo: function (baseTo) {
}
}
}
}
}
convertBase(num, from, to);
let convertBase = (num, baseFrom, baseTo) => {
return parseInt(num, baseFrom).toString(baseTo);
}
它提供了一个 fluent interface 来明确哪个值去哪里。
convert(3).fromBase(16).toBase(2);
严格来说比
更好(更易维护、更易读、更不容易出错)convertBase(3, 16, 2);
其中 3 个整数参数的顺序不明显。
从另一个函数返回一个函数的基本概念称为闭包。
闭包的概念可以应用于部分应用和柯里化。
你可以阅读它们 here
它有适当的例子说明为什么嵌套函数更好。
这与其说是嵌套函数,不如说是 schönfinkeling / currying。 Schönfinkeling / currying 以开发此技术的 Moses Schönfinkel(在 Gottlob Frege 之前介绍它之后)和完善并描述它的 Haskell Curry 的名字命名。
简单来说,柯里化是一种技术,可以将任何 n 个参数的函数转换为 n-1 个参数的函数其中 returns 一个接受 nth 参数的函数。通过反复应用这一点,您可以证明您永远不需要具有多个参数的函数来为具有任意多个参数的函数建模。
这是一个例子。我可以转换一个将两个数字相加的函数:
function add(a, b) { return a + b; }
add(2, 3)
//=> 5
进入 "adder factory" 那个 returns 加法器函数,当调用它时将产生两个数字的总和:
function adderFactory(a) {
return function adder(b) { return a + b; };
}
const twoAdder = adderFactory(2);
twoAdder(3)
//=> 5
或
adderFactory(2)(3)
//=> 5
现在,您可能会想:"but ECMAScript supports functions with more than one argument, so why would I simulate them using currying, if I can have them natively?"您是对的!出于这个原因使用柯里化是没有意义的。
但是,您可能还想用函数做另一件有趣的事情:部分应用。 "Function application" 只是函数式编程代表 "calling a function",所以 "partial application" 意味着 "calling a function with only a subset of its arguments"。部分应用仅使用其部分参数调用一个函数,并生成一个仅针对这些参数 specialized 的函数。在支持部分应用的语言中,我可以这样做:
const fourAdder = add(4, ?);
但是,ECMAScript 没有部分应用。
然而,当我柯里化我的函数时,我可以做 "sort-of partial application",我至少可以只提供前几个参数,而忽略最后几个参数。这意味着你必须考虑哪些参数更可能是固定的,哪些参数更可能是可变的,并且你应该按 "variability".
对它们进行排序因此,对于您发布的函数,可以创建一个只能将一个特定数字从一个特定基数转换为任意数量基数的基数转换器。我必须承认,这实际上并不是很有用。如果像这样定义函数会更有用:
const convertFromBaseToBase = baseFrom =>
baseTo =>
num => parseInt(num, baseFrom).toString(baseTo);
convertFromBaseToBase(2)(8)('1001')
//=> '11'
现在,例如,您可以像这样创建一个从八进制到十六进制的转换器:
const octalToHexadecimalConverter = convertFromBaseToBase(8)(16);
octalToHexadecimalConverter('17')
//=> "F"
注意!由于你只能 "partially apply from the right" 的限制,你实际上也可以使用带有默认参数的可选参数来做到这一点,有点像这样:
const baseToToken = Symbol('baseTo'),
numToken = Symbol('num');
function convertFromBaseToBase(baseFrom, baseTo=baseToToken, num=numToken) {
if (num === numToken) {
if (baseTo === baseToToken) {
return (baseTo, num=numToken) =>
num === numToken ?
num => parseInt(num, baseFrom).toString(baseTo) :
parseInt(num, baseFrom).toString(baseTo);
} else {
return num => parseInt(num, baseFrom).toString(baseTo);
}
} else {
return parseInt(num, baseFrom).toString(baseTo);
}
}
convertFromBaseToBase(8, 16, '17')
//=> 'F'
convertFromBaseToBase(8, 16)('17')
//=> 'F'
convertFromBaseToBase(8)(16)('17')
//=> 'F'
convertFromBaseToBase(8)(16, '17')
//=> 'F'
但是,如您所见,这开始变得非常丑陋,非常快。
问题中的代码片段还有一个有用的原因:它提供了一个 流畅的界面 为特定参数命名,这样您就不会混淆这两个数字参数baseFrom
和 baseTo
。然而,这也可以通过其他几种方式解决。一种是通过命名函数,以便清楚 baseFrom
或 baseTo
是第一个,即 convertBase(num, baseFrom, baseTo)
而不是 convertNumberFromBaseToBase(num, baseFrom, baseTo)
。另一种可能性是使用对象参数,如下所示:
function convertBase({ num, baseFrom, baseTo }) {
return parseInt(num, baseFrom).toString(baseTo);
}
convertBase({ num: '17', baseFrom: 8, baseTo: 16 })
//=> 'F'
但是,即使使用更具描述性的函数名称或流畅的界面,更改参数的顺序仍然有意义,以使柯里化和部分应用更有用。
另请注意,我根本没有说任何关于 不是 用于柯里化的嵌套函数,例如在本例中 [代码改编自 Ruby Recursive Indexing/Searching Method (Using Middle Comparison) Returning Incorrect Index Value]:
function bsearch(arr, target) {
function bsearchRec(arr, target, offset=0) {
const middleIndex = Math.floor(arr.length / 2);
if (arr[middleIndex] === target) { return offset + middleIndex; }
if (arr.length === 1) { return undefined; }
if (target > arr[middleIndex]) {
return bsearchRec(arr.slice(middleIndex+1), target, offset + middleIndex + 1);
} else if (target < arr[middleIndex]) {
return bsearchRec(arr.slice(0, middleIndex), target, offset);
}
}
return bsearchRec(arr, target);
}
bsearch([1, 3, 4, 5, 9], 5)
//=> 3
这里,嵌套函数bsearchRec
嵌套在bsearch
内部,因为它是bsearch
的私有内部实现细节,除了bsearch
的作者之外,没有人应该知道了。
最后,函数是 ECMAScript 中用于封装的载体。特别是,函数是 ECMAScript 实现对象的方式。对象具有由名称和封装标识的行为。在大多数 OO 语言中,行为、封装和名称到行为的映射(又名 "method calls")这三样东西由一个实体(对象)提供。在 ECMAScript 中,封装由函数(闭包)提供,行为由函数提供(嵌套在闭包内以共享私有状态),从名称到行为的映射由字典提供,它们被混淆地称为 objects,尽管它们只实现了对象的三分之一。
所以,如果没有嵌套函数,ECMAScript 中就没有封装,最重要的是,没有对象!甚至模块和 类 也主要是嵌套函数之上的语法糖。