深入理解函数

关于函数的概念,在此不多作解释。简单来说,函数就像是一个暗箱,把计算过程封装在暗箱中,再次调用函数时,只需要传入必要的参数,就能按照既定的规则返回结果。
在M语言中,函数主要有内建函数,比如Text.From这种系统自带的;自定义函数,形如(x)=>x+1这种;以及参数函数,即函数内参数类型为function的,function就是函数的意思了。

我们先来看自定义函数,比如fx = (x)=>x+1。创建一个名为fx的函数,需要一个参数x,传入参数后返回结果为x+1,比如fx(5),将5带入函数,返回结果为6,这很容易理解。
关于自定义函数,之前在《自定义函数》中已经讲过一些供参考。

再来看一个简单的例子:点击添加列-自定义列,直接输入1,点击确定。

我们看到Table.AddColumn的第三参数,虽然我们输入的是1,但是系统自动在1前面加上了一个each。
那么问题来了,这个each是什么东西?
再看一眼Table.AddColumn第三参数的类型,显示为function。单独一个1是number,加个each就变成function了,这个怎么理解?
我们单独在编辑栏里输入each 1看一下:

发现这么写并没有报错,并且底部以及左侧的图标都表明这是一个function。
我们注意到还给了一个可选参数为_,这个_又是什么?
在此情况下,无论我们输入_等于什么,返回的结果都是1,因为_并没有参与函数的运算,于是把_加入右边的表达式中试一下,fx=each _+1

此时发现,当我们给任意一个数字时,返回的结果都会是我们给的参数+1,比如fx(5),返回结果为6。
咦,这不正是本文开头所讲的自定义函数的效果么?
没错,当自定义函数只有一个参数时,(x)=>x的写法完全与each _等价。
所以,可以把刚才添加列中的第三参数里的each 1,写成(x)=>1,效果完全一样。

OK,这个理解了我们继续往下看。
如果刚才添加列输入的不是1而是_呢?

我们已经知道了each _就相当于(x)=>x,而(x)=>x看上去好像更容易理解一些,就是你传入的是什么,返回的还是什么。
那么在= Table.AddColumn(源, "自定义", each _)中,传入的是什么?是源这张表,但是因为上下文的原因,这张表被拆分成了很多行,所以每一行只返回当前行的所有列,结果为一个record。
而record又能深化出其中的字段,比如我们要其中[索引]字段下的值,那么可以_[索引],第一行返回的结果为1。
所以我们平时添加列,比如要[索引]*10,公式为= Table.AddColumn(源, "自定义", each [索引]*10),而在这个[索引]的前面,实际上是省略了一个_的:

当然如果你用的是each,此处的_可以省略,但是如果你写成(x)=>的形式,就不能省略,要写成= Table.AddColumn(源, "自定义", (x)=> x[索引]*10)

再说一遍,当函数传入的参数只有一个时,each _与(x)=>x等价。那么既然等价,什么时候该用什么呢?
这个问题问得好!其实两个都能用,用什么那只是习惯问题。
each是语法糖,一般来说,当函数作为参数的时候习惯用each _,这样能够提高代码的可读性;而当单独定义一个函数的时候习惯用(x)=>x。
但是有一种情况比较特殊,在自定义函数那一篇中,我介绍过一个形如lambda表达式的匿名函数来实现累计求和,很多人表示不理解。再来回顾一下:

整体的思路就是,先筛选出表中[索引]<=当前行的索引的所有记录,比如当前行索引为3,那么得到的就是索引为1,2,3的三行,然后对筛选表中的[索引]列求和,即可得到累计求和。 先撇开求和不谈,先来看筛选表,刚才说过这里的<=[索引]实际上省略了_,我们先加上,公式为= Table.AddColumn(源, "累计", each Table.SelectRows(源,(x)=>x[索引]<=_[索引]))

其中用到两个函数Table.AddColumnTable.SelectRows,先单独看下这两个公式:

= Table.AddColumn(源, "累计", each 源)
= Table.SelectRows(源,each [索引]<=3])

而现在我们要的是把前面公式中最后的源,传递到后面公式最后的3的位置,所以要把两个公式嵌套在一起,而这两个函数中都有类型为function的参数,所以如果都用each _,到后面就会变成each _[索引]<=_[索引],那么就会指代不明,你说这个_到底是前面的函数里的呢,还是后面函数里的呢?而如果这两个_表示的是同一个意思即<=左右两边相等,那么这个表达式的结果恒为true,返回的结果就会是整张表,显然不是我们想要的。 所以,在遇到两个或多个函数的参数互相干扰的时候,需要使用不同的参数将它们区分开来。 当然除了以上的写法,你也可以写成以下的形式,把三个放一起比较下区别:

= Table.AddColumn(源, "累计", each Table.SelectRows(源,(x)=>x[索引]<=_[索引]))
= Table.AddColumn(源, "累计", (x)=> Table.SelectRows(源,each _[索引]<=x[索引]))
= Table.AddColumn(源, "累计", (x)=> Table.SelectRows(源,(y)=>y[索引]<=x[索引]))

这三种写法完全等价。

最难的又理解了,我们再回过头看简单的以便彻底理解。当时还给了一个分步创建自定义函数的写法:

let
    fx =(x)=>Table.SelectRows(源, each [索引] <= x),
    源 = Excel.CurrentWorkbook(){[Name="表1"]}[Content],
    筛选 = Table.AddColumn(源, "累计",each fx([索引]))
in
    筛选

这样应该都能理解,那如果我们把第一步的自定义函数改一下呢?改成fx = each Table.SelectRows(源, (x)=> x[索引] <= _),最后的结果完全一样。
那我们第三步里写的是fx([索引]),fx是自定义函数的函数名,而fx又等于第一步等号右边的表达式,那直接用表达式来替换fx可不可以呢?fx([索引])传入的参数为[索引],那么在表达式中把的形参_替换为实参[索引],写成= Table.AddColumn(源, "累计",each Table.SelectRows(源, (x)=> x[索引] <= _[索引]))
看!替换完得到的结果,不就是我们上面写的那个吗?

再反过来,你甚至可以把_[索引]写到自定义函数里,第一步写成fx = each Table.SelectRows(源, (x)=> x[索引] <= _[索引])。这是名为fx的函数,而第三步Table.AddColumn的第三参数要的就是function,于是你可以直接给一个函数,写成筛选 = Table.AddColumn(源, "累计",fx),结果也完全正确。
所以当函数需要传入的参数为当前上下文本身且不需要其他参数的时候,你可以直接丢一个函数名,连each都不用,比如= List.Transform({1..5},Text.From)。因为each Text.From(_),意思就是传递一个参数然后调用函数,那不就还是原来的函数嘛,所以何必多此一举。

除了以上提到的几个函数,在M语言中还有很多函数的参数类型为function,正是由于我们可以对function进行任意的自定义改造,所以你会发现这些函数都比较难理解,但是又有无限的可能,可以从中挖掘到很多常规函数的非常规用法,这也是M语言的魅力所在。

本文介绍了函数的几种形态,以及之间的相互联系与转换。对于初学者来说可能有点绕,但是如果多看几遍并自己动手测试下,把这篇深入理解,将会一通百通。

54 Replies to “深入理解函数”

  1. 好不容易 理解了 fx = each Table.SelectRows(源, (x)=> x[索引] <= _[索引])的
    <= _[索引

    看来 each 跟个_ 理解起来容易点

    哎呀 白头发又多了一条

  2. 请问,在一个被嵌套的let中访问和修改外面的let的参数的方法是什么。尤其是在function中的each后面访问和修改高一级别的环境中的参数。
    let
    a=1,
    c=let
    b=1,
    @a=1
    in @a
    in c
    另外有没有方法修改系统的自定义参数。
    另外有没有方法批量导入已经写好了的数据块和函数块?谢谢哈

    1. 没看明白你的意思,你给的代码的语法也是不对的,请举例说明问题。
      另外PQ中的变量赋值是不可变型,比如let a=1,a=2 in a这是不可以的,不能对之前已经赋值的变量修改。

  3. 请问一下这些教程的编写跟排版以及构思都是博主一个人做出来的吗?太厉害了吧

  4. (x)=>x[索引]<=_[索引]
    筛选出表中[索引]<=当前行的索引的所有记录,比如当前行索引为3,那么得到的就是索引为1,2,3的三行,然后对筛选表中的[索引]列求和

    这个相当于多对一,为什么不是多对多呢,完全不懂啊

      1. 谢谢你的回复
        我大概有些理解了,但是是相当于函数本身的性质,Table.AddColumn(源,"自定义",each _)添加出来的的新列每个内容是一个record,若是each_[索引]返回结果是索引号,Table.SelectRows(源,each [索引]<=3])的返回结果是一个table,即根据函数本身的性质,Table.SelectRows(源,each [索引]<=3])里面的each [索引]就是索引那一整列,而Table.AddColumn(源,""自定义"",each _[索引])里面的each [索引]就是返回索引那一整列的某个值
        "对于整张表来说是多对多,但是受到行上下文的筛选,对于每一行来说就是一对多"
        到行上下文的筛选 这句我不太懂可不可以说深入一些
        谢谢

        1. 上下文就是所处的环境,对于整张表来说[索引]表示的应该是一列。
          但是比如对于第4行来说,[索引]表示的是当前的第4行与索引列的行列交叉处的单值为4,到了第5行又变成了5。
          表达式是一样的,但是在不同环境下表示的值却不一样,这就是上下文。

          1. 意思就是说:
            = Table.AddColumn(源, "累计", each Table.SelectRows(源,each _[索引]<=_[索引]))
            在Table.SelectRows这个环境下each的_[索引]就是整张表(源)的索引,后面的_[索引]不会指代到Table.AddColumn环境下each的下文内容(即每行记录),each __[索引]<=__[索引]两个内容一样就永远返回true,返回了所有内容,而我们想要得到的是想要x[索引] Table.SelectRows(源,each _[索引] Table.SelectRows(源,(y)=>y[索引]<=x[索引]))
            这样理解对吗?

    1. 源是一张表,each 源就是给表中每一行都添加一个内容是源这张表的列。
      光看没有用,写出来就懂了。

  5. = Table.AddColumn(源, "累计", each Table.SelectRows(源,(x)=>x[索引] Table.SelectRows(源,each _[索引] Table.SelectRows(源,(y)=>y[索引]<=x[索引]))
    这三种,哪种最好理解,我觉得是第二种,这里的x的含义很清楚,就是指当前行。不过我我要请问一下,第一种,和第三种里的x和x,y分别代表是什么含义呢。M语言为什么要搞成这么多路径去实现同样的功能呢?

    1. 今天由于工作需要,又看了一下自定义函数,终于明白一件事,Table.AddColumn(已添加索引, "累计求和", (y)=> List.Sum(Table.SelectRows(已添加索引,(x)=>x[索引]<=y[索引])[索引]))源是表,里面有两个源是有两张表,就是一个表和自已的内联结,主表是外面那个源,从表是里面那个源,从表索引少于主表索引的行并对索引求和,这是这个公式的含义。

  6. 刚学习,也看到上面的上下文的解释,我自己的理解的是,两个函数第一参数都是表(Table),即认为输入的都是表,Table.AddColumn函数中第三参数columngenerator将每行作为输入,所以each_表示输入表的每一行,each_[索引]便是的每一行与[索引]字段的交叉,就是索引列的单个值。Table.SelectRows同样作是表输入,后面条件即是从所有行中选满足具体条件的行,所以Table.SelectRows(源,(x)=>x[索引]<=_[索引]),即是所有行与索引的交叉,即索引那一列,二后面需要小于具体的值_[索引],就进一步表示坐在行与索引那一列的交叉。上下具体还是根据函数本身的性质,以及函数最终要实现的目的,同样的公式表达不一样的意思,也不知道说的对不对,打这么字也只是为了加深自己的理解~~不过总感觉这个上文概念是比较模糊或者说模棱两可吧。

  7. 刚学习函数,我理解记得上下文是根据函数参数的性质,以及函数最终要实现的目的来理解的,Table.AddColumn(源, "自定义", each_[索引]),这个函数第三参数column generator是默认将每行作为输入,所以each_是指的源中的所在行即Record,each_[索引]即是所在行与索引列的交叉值,Table.SelectRows(源,each [索引] x[索引] x源中的所有行与[索引]列交叉得到一列的值,(x)=> x[索引] 需要符合具体的值,所以 _[索引]表示所在行与[索引]的交叉的具体值。所以感觉each_或者(x)=> x需要根据函数的参数性质和具体的实现目的来理解,也不知道说的对不对,总感觉理解的有点牵强,写下来也是为了加强理解。最后谢谢楼主的好文。
    另外想问一下,这个评论,是需要注册什么才能录入评论么,姓名和电子邮件是指的注册时的昵称和邮箱么?总是评论失败

  8. 想请教下,关于 x[索引] 这种深化,其中“索引”这个字段名称,可以变为一个变量引用吗,是想在自定义函数中,根据参数选择从哪一列取数。

  9. 也是一直卡在(x)=>x[索引]<=_[索引] 这里,搞不懂这个写法具体要怎么理解和解释,x[索引]是表示对索引列深化吗?

  10. 注意每个函数传入的是什么,嵌套的不同函数上下文不一样,这样理解(x)=>和each _会清晰一些。同一个上下文环境,可以省掉each _。

  11. 先mark下。看的头晕眼花。
    对M 函数 变量传递的机制 隐约了解,但细想或应用还是不懂。需要再深入的学习。

  12. 感谢老师的分享!
    有一点没弄明白的是each所传递值是什么;
    1.= Table.AddColumn(Resource,"x", each [a])
    这个很容理解是传递了Resource的Table并调用了字段“a”,将a的值赋予给新字段”x”。
    2.= Table.AddColumn(Resource,"x", each Resource2)
    到这个的话就不太清楚了,我又建立了一个”Resource2”,那么得到的结果是将”Resource2”里面的所有内容以Table的形式全部赋予给了”x”字段,这个时候each被传递的是什么参数?
    3.= Table.TransformColumns(Resource, {{"a", each _}})
    在这个情况的时候得到的结果是“a”字段本身的值不是一个Record。
    有些东西还是没有禅悟到,希望老师指点!非常感谢

    1. 看语境。
      简单来说,你先试试 Table.AddColumn(源,"新列", each _), 增加的是什么。你会发现增加的是 这一行的record。所以同样,你把each _改成 Table.AddColumn(源,"新列", (x)=>x), 效果也是一样。
      如果函数是 List.Transform({1..10}, each _), 这里的 _ 就是前面列表中的每一个值。
      针对更复杂的函数也是同理: List.Accumulate({1..10},{},(x,y)=>x )

    2. 如果把each读作“每一”,那 Table.AddColumn(Resource,"x", each [a])就是“每一”resource中的a,因为有上下文,所以变成了每一个a
      同样理解,Table.AddColumn(Resource,"x", each Resource2),就是“每一”resource中的resourse2,但此时没有上下文限制,或者说限制不到,所以就整个取出来了
      同样理解,Table.TransformColumns(Resource, {{"a", each _}}),提取“每一”a

      我粗浅的理解,这个上下文限制是怎么回事,我也不太清楚

  13. Table和List的函数所操弄的Table或List就是一个数据环境,each 值 就是把值返回给这个环境,源也好,_也好,都是值;each意即“每个、每一”,和它对应的_就是“这一”;each 值 把值返回给环境中的每个元素,再具体点,就是返回给当前元素_,至到所有元素遍历完。each 值 是一个函数,这个值也可以由另外的函数返回,同理,这个所谓另外的函数所操弄的Table或List也是一个数据环境,所谓上下文环境,获取值的是上文,提供返回值的是下文。可见上下文是相对的,是相辅相成的,给这个下文再套个下文,它也就成了它的下文的上文。

  14. = Table.AddColumn(源, "累计",(y) =>List.Sum( Table.SelectRows(源,(x)=>x[索引]<=y[索引])[索引]))
    所以M函数的读取,是从外到内读取的,所以先定义的y,到嵌套函数里面也能正常读取?

  15. let
    源 = Excel.CurrentWorkbook(){[Name="表1"]}[Content],
    fx = (y)=> Table.SelectRows(源, (x)=> x[索引] x[索引] x, 都是被行上下文逐个输出, 但是外层函数是 Table.SelectRows, 所以会输出表
    */

    /*
    关于 function 要求, 只要求输入函数, 所以可以只输入 fx
    感谢老师分享,我应该是悟了
    */

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注