玩转 Lambda 函数
原文链接:https://www.nv5geospatialsoftware.com/Learn/Blogs/Blog-Details/fun-with-lambda-functions
9440 Rate this article:
No rating
玩转 Lambda 函数
匿名 星期三,2015年1月21日
IDL 8.4 中我一直在钻研的一个新功能是 Lambda() 函数。虽然帮助文档中有一些不错的示例,但我想分享一些我在实际应用中的例子以及学到的一些经验。
我遇到的一个简单例子是:获取一个我知道所有键都是字符串的哈希,并创建一个包含它们全部大写形式的列表。有几种方法可以实现这个目标。在 8.4 版本之前,实现这个功能需要几行代码:
h = Hash('foo', 1, 'bar', 2, 'baz', 3)
keys = h.Keys()
upperArray = StrArr(keys.Count())
foreach key, keys, i do upperArray[i] = StrUpCase(key)
upperKeys = List(upperArray, /EXTRACT)
还不错,但是有了 IDL 8.4 中引入的静态方法,我们可以做得更好一些:
h = Hash('foo', 1, 'bar', 2, 'baz', 3)
keys = h.Keys()
keyArray = keys.ToArray()
upperArray = keyArray.ToUpper()
upperKeys = List(upperArray, /EXTRACT)
这至少使用了 ToUpper() 静态方法来向量化整个数组的转换,而不是对每个元素使用 foreach 循环。但是有了 Lambda 函数和 List::Map () 方法,我们可以做得更好:
h = Hash('foo', 1, 'bar', 2, 'baz', 3)
keys = h.Keys()
upperKeys = keys.Map(Lambda(str: str.ToUpper()))
Lambda 函数会应用到列表的每个元素上,其输出结果用作输出列表中的对应元素。在这个例子中,Lambda 函数接收输入的列表元素,并对其调用 ToUpper() 静态方法。必须注意源列表只包含字符串,否则会出现问题:
IDL> h = Hash('foo', 1, 2, 'bar', 'baz', 3)
IDL> keys = h.Keys()
IDL> upperKeys = keys.Map(Lambda(str: str.ToUpper()))
% Attempt to call undefined method: 'IDL_INT::TOUPPER'.
% Execution halted at: $MAIN$
这里,哈希有一个整数键,Lambda 函数试图在 IDL_Int 变量上调用 ToUpper(),而不是在 IDL_String 变量上。
我们继续看我在处理 ENVITask 以及 SetProperty 和 GetProperty 能够根据 ENVITask 类的属性列表和任务拥有的参数来区分 _REF_EXTRA 关键字时创建的一对 Lambda 函数。第一个 Lambda 函数用于识别所有以给定字符串开头的列表元素,第二个用于识别所有是给定字符串初始子串的列表元素。我将从一个特定的输入字符串开始,然后推广到适用于任何字符串。这是可以用来查找列表中所有以子串 'ENVI' 开头的字符串的 Lambda 函数:
IDL> substringMatch = Lambda(listElem: listElem.StartsWith('ENVI'))
IDL> l = List('ENVIRaster', 'Gaussian', 'ENVIVector', 'Laplacian')
IDL> l2 = l.Filter(substringMatch)
IDL> l2
[
"ENVIRaster",
"ENVIVector"
]
Lambda 函数使用新的静态方法 StartsWith() 来返回当前列表元素是否以字符串 'ENVI' 开头的布尔状态。我们本可以在 StartsWith() 调用中使用 /FOLD_CASE 关键字来使字符串比较不区分大小写。
这是可以用来查找列表中所有是字符串 'ENVIRaster' 子串的字符串的 Lambda 函数:
IDL> startsWithSubstring = Lambda(listElem: ('ENVIRaster').StartsWith(listElem))
IDL> l = List('EN', 'ENVIRas', 'ENVIVector', 'ENVIRaster', 'ENVIRasterSpatialRef')
IDL> l2 = l.Filter(startsWithSubstring)
IDL> l2
[
"EN",
"ENVIRas",
"ENVIRaster"
]
这里,Lambda 函数使用了一个技巧:将字符串字面量放在括号内,以便可以在其上调用静态方法。
这些都很好,但如果我有许多不同的字符串想要进行比较,我就必须为每一个都写新的 Lambda 函数定义。相反,我想要一个参数化的 Lambda 函数,它使用变量而不是字符串字面量作为搜索字符串。然而,我们不能简单地在 Lambda 函数定义中添加第二个参数:
IDL> substringMatch = Lambda(listElem, substring: listElem.StartsWith(substring))
IDL> l = List('ENVIRaster', 'Gaussian', 'ENVIVector', 'Laplacian')
IDL> l2 = l.Filter(substringMatch('ENVI'))
% IDL_STRING::STARTSWITH: String expression required in this context: SUBSTRING.
% Execution halted at: IDL$LAMBDAF4 1 <Command Input Line>
% $MAIN$
Lambda() 函数会为你创建一个有效的 lambda 函数,但它并不按预期工作,并且肯定不能在 List::Filter() 方法中工作。问题是分号前的 'substring' 被视为输入变量,所以当它调用 StartsWith() 静态方法时,期望该变量已被定义。同时,你也不能像对待单变量函数那样使用 substringMatch 变量,传入想要的字符串值,因为它被定义时带有两个参数。但还有希望,正如 Lambda 函数帮助文档在其"更多示例"部分所演示的那样,你可以创建一个 Lambda 函数,该函数反过来生成另一个嵌入了所需字符串字面量的 Lambda 函数。以下是通用的子串匹配 Lambda 函数的实际应用:
IDL> substringMatch = Lambda('subString: Lambda("listElem: listElem.StartsWith(''"+subString+"'')")')
IDL> l = List('ENVIRaster', 'Gaussian', 'ENVIVector', 'Laplacian')
IDL> l2 = l.Filter(substringMatch('ENVI'))
IDL> l2
[
"ENVIRaster",
"ENVIVector"
]
这个新的 Lambda 看起来很乱,但可以分解和分析。我们注意到的第一件事是,外层的 Lambda 被传递了一个字符串字面量,而不是未加引号的代码。这是嵌套 Lambda 语法正常工作所必需的。现在让我们看看打印到控制台时那个字符串是什么:
IDL> print, 'subString: Lambda("listElem: listElem.StartsWith(''"+subString+"'')")'
subString: Lambda("listElem: listElem.StartsWith('"+subString+"')")
外层的 Lambda 函数接收其输入参数(我命名为 'subString'),并将其传递给另一个对 Lambda() 的调用。这个内层的 Lambda 函数连接三个字符串:两个使用双引号的字符串字面量以及 subString 的值。我们还会注意到,这两个字符串字面量包含一个单引号,以便在计算内层 Lambda 函数时,subString 变量的值被转换为字符串字面量。回顾外层 Lambda 的原始声明,我们看到一个用单引号引起来的字符串字面量,里面有一些用双引号引起来的字符串字面量,每个里面都包含单引号字符,单引号字符可以转义为一对单引号。有点乱,但坐下来拆解分析时就能明白它是如何工作的。
那么,以下是通用的超字符串匹配 Lambda 函数的实际应用:
IDL> startsWithSubstring = Lambda('superString: Lambda("listElem: (''"+superString+"'').StartsWith(listElem)")')
IDL> l = List('EN', 'ENVIRas', 'ENVIVector', 'ENVIRaster', 'ENVIRasterSpatialRef')
IDL> l2 = l.Filter(startsWithSubstring('ENVIRaster'))
IDL> l2
[
"EN",
"ENVIRas",
"ENVIRaster"
]
与上面类似的分析将证明这个嵌套的 Lambda 函数是如何工作的。
在代码中使用这个嵌套 Lambda 函数概念时,有两个重要的注意事项。首先,如果你想在控制台窗口中试验它们,一开始会遇到像这样的奇怪错误:
IDL> substringMatch = Lambda('subString: Lambda("listElem: listElem.StartsWith(''"+subString+"'')")')
IDL> l = List('ENVIRaster', 'Gaussian', 'ENVIVector', 'Laplacian')
IDL> l2 = l.Filter(substringMatch('ENVI'))
% Type conversion error: Unable to convert given STRING to Long64.
% Detected at: $MAIN$
原因是解释器会认为 substringMatch 变量是一个字符串,而不是一个函数,所以它认为你使用的是老式的括号数组索引语法。这个错误信息在这方面相当隐晦,但这正是它要表达的意思。你可以通过在控制台窗口中简单地输入 compile_opt idl2,然后重新运行 l2 这行代码来修复这个问题。这说明了如何将 compile_opt 语法用于改变主解释器会话的行为,而不仅仅是在例程内部。它还表明,你需要确保在任何使用 Lambda 函数的例程中都包含 compile_opt。
第二个注意事项是,如果你想在要编译到保存文件中的代码中使用这样的嵌套 Lambda 函数,那么你需要做一点额外的工作。如果你尝试使用上面的代码,编译它,然后在调用 SAVE 之前调用 RESOLVE_ALL,那么你会得到这样的错误信息:
Attempt to call undefined function: 'STARTSWITHSUBSTRING'.
Attempt to call undefined function: 'SUBSTRINGMATCH'.
解决方法是,将任何像这样用于保存嵌套 Lambda 函数值的变量名添加到一个数组中,并将该数组传递给 RESOLVE_ALL 的 SKIP_ROUTINES 关键字。一旦你这样做了,保存文件就会构建成功,并且在执行代码时,它应该能正常工作。