Power Query 学习 01 关于 each _的深入理解和应用要点

前言:函数型参数的使用,可以说是PQ应用的一道分水岭。而each _ 表达式,是函数参数最常用的写法。想用好函数参数,就必须理解和掌握each _ 的含义和使用方法。关于each _和函数表达式的关系,施神的帖子已经有了比较详细的论述,https://pqfans.com/1726.html ,如果你已经完全理解掌握了此帖,本文就不用看了。本文仅针对一时(或一直)看不懂施神帖子的初学者,为大家打个入门基础,看懂了好再去看大神的帖子。


正文:要想理解each _的含义,先要知道它是什么,PQ的数据类型大致可分为三类:

单值类:null,number,text,logical
容器类:list,record,table
函数类:(x) => x,Number.From

奇怪的是这里面并没有each _,没办法只能查官方语法书,我节选了关键原文如下:

9.7 Simplified declarations
The each-expression is a syntactic shortand for declaring untyped functions taking a single formal parameter named _ (underscore).
Simplified declarations are commonly used to improve the readability of higher-order function invocation.

鉴于只有这么两句话,我就恬着脸翻译一下:

9.7 简化声明
each表达式是一种简化的语法声明,用于定义一个自定义函数,函数包含唯一的、未定义类型的参数,参数名为_ (下划线)。
简化声明通常用于提高高阶(嵌套)函数的可读性。

原来人家已经说得很清楚了:
1、each表达式就是声明一个自定义函数
2、它有唯一一个参数,参数名为下划线
3、它主要用于简化书写嵌套函数的函数型参数
So,each表达式就是简写的函数表达式,所以只有用函数的地方才能写哦。下面再看一下语法书里的示例:
示例1:

each _ + 1 
(_) => _ + 1

通过示例文件,我们就看的更清楚了,原来each就等效于(_) =>,注意这里的下划线也是固定的,也就是写出each就已经确定了唯一的变量名为下划线的参数,下面我们新建一个空查询,在里面输入each _验证一下:
file
果真返回了以下划线为参数名的函数,我们把表达式改为(_) => _ ,可以发现返回结果完全相同,这也进一步验证上面的结论。

下面我们再看看示例2:

each [A] 
(_) => _[A]

仔细观察不难发现each表达式后面需要进行查找深化引用时,可以省略下划线,直接写成each [字段名]的模式。下面我们进一步验证这种模式,可以发现下面这5个表达式是完全等效的。

each [A]
each _[A]
(_) => [A]
(_) => _[A]
(x) => x[A]

接着再看示例3,我们就能直观的体会到这种简写的方便性和易读性了。

Table.SelectRows( aTable, each [Weight] > 12 ) 
Table.SelectRows( aTable, (_) => _[Weight] > 12 )

好,看懂上面这些,我们就了解了each表达式的含义,下面我们再研究一下它的使用特点。首先我们在常规自定义函数中试验一下:
file
正常使用,完全没有问题。接下来就是本文的重点:each表达式在函数型参数位置的使用要点:


先看案例1,each用于List.TransformMany,还是先看这个函数的官方帮助:

List.TransformMany(list as list, collectionTransform as Function, resultTransform as Function) as list

通过语法说明,可以看到,此函数的2、3参数都是函数型。2参数位置的函数,传递的是一个变量参数,内容是遍历1参数List的每一项。3参数函数则有两个变量参数,为1、2参数List元素的每一种组合,也就是笛卡尔积(关于此函数,有兴趣可参看施神相帖子,本文不做详细介绍)。
回到本文主题,下面,我们就分别试验把2、3参数的函数都写为each _的形式:
file
file
可以发现2参数没问题,3参数的函数,返回了参数数量不符的错误,也就是2个参数传递了过去,而each声明的函数只有一个参数,所以报了错。由此可见each模式声明的函数,只能用于单变量参数传递的情况,而多变量传递时无法适用。


再来看案例2,嵌套函数中each表达式的使用,为了易于理解,我们就用两个最简单的List.Transform嵌套,还是先看语法:

List.Transform(list as list, transform as function)  as list

第2参数是很典型的函数型参数,正适合做实验。为了进行两层嵌套转换,我们需要一个两层的List嵌套型数据源,也就是形如:{{...},{...}...}的Lists,先建立结构如下:

= List.Transform({{1,2,3},{4,5,6},{7,8,9}},each List.Transform(_,each _))

file
可以发现,虽然内外层都使用了each _,函数并没有什么解析错误,理解的好好的。再进一步增加操作,外层每一个List去掉第一个值,内层则每一项+1:
file
还是完美实现,为什么使用了相同的参数变量名_,却没有造成冲突呢?这就要先了解一下PQ的变量使用范围规则,通俗的理解大致如下:
1、每层嵌套都可以定义变量,可以和其它层重名,不会冲突
2、遇到未命名的变量引用,可以向外层查询,并向内层传递,如果查不到则会报错
光说说不清楚,举两个例子:

//例1:
let 
    a=1,
    b=  let
            a=2,
            b=3
        in
            a+b
in
    a+b
//输出:6

//例2:
let 
    a=1,
    b=  let
            b=3
        in
            a+b
in
    a+b
//输出:5

例1中:内层嵌套输出2+3=5给外层的b赋值,外层再返回:1+5=6
例2中:内层没有定义a,直接引用。这样就会向外层查找,外层的a就传入内层,因此内层嵌套输出1+3=4给外层的b赋值,外层再返回:1+4=5
理解了这两个例子,再看之前的函数嵌套:
file
图里红框圈出了两层函数的each表达式的范围,这样便很容易理解,在内层的下划线,代表传递进来的List的每个内层元素,外层的下划线则代表每个List,所以PQ并不会产生解析的逻辑错误。
那到底什么情况会产生问题呢?好,我们现在想把每个内层元素加上它所在List的中间项,即:
{1,2,3} => {1+2,2+2,3+2}
{4,5,6} => {4+5,5+5,6+5}
{7,8,9} => {7+8,8+8,9+8}
要实现这样的功能,我们就需要在内层表达式里同时调用:内层变量+外层变量的中间项,如果仍然都用each,写出的表达式为:
= List.Transform({{1,2,3},{4,5,6},{7,8,9}},each List.Transform(_,each _ + _{1}))
file
file
我们发现终于报错了,错误内容是无法将数值7转化为List
file
通过对刚才的案例的理解,这个错误不难分析。示意图中圈出了两个内层嵌套的下划线,传递的是内层的每个数字。前面的引用没问题,而后面的下划线,我们想要传递的是外曾的变量,也就是每个List,并取出它的第2项。这时PQ就无法正确理解我们的意图,仍按规则,对内层数值取值并深化,最终产生了错误。
那咋办尼?很简单,要避免PQ理解错误,我们就得对内外层使用不同的变量名,那随便改掉一个each表达式,或者干脆都改掉,就不会有歧义了,下面给出了3种改法。

= List.Transform({{1,2,3},{4,5,6},{7,8,9}},each List.Transform(_,(x) => x + _{1}))
= List.Transform({{1,2,3},{4,5,6},{7,8,9}},(x) => List.Transform(x,each _ + x{1}))
= List.Transform({{1,2,3},{4,5,6},{7,8,9}},(x)=> List.Transform(x,(y) => y + x{1}))

file
都没问题,效果完全一致。


总结:

1、each 表达式,是简化的函数声明,等效于(_) =>
2、each 表达式的变量名为_,在查找深化引用时,_可以省略不写
3、each 表达式因为只有一个参数变量,无法应用于多参数传递的情况
4、多层嵌套,内层嵌套需同时引用外部变量时,不能内外部同时使用each表达式,会造成引用不明,需要修改其中一个表达式为普通模式。

是不是豁然开朗了,还是一头雾水,哈哈哈。