ShoneSharp(S#炫语言)

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

ShoneSharp语言(S#)的设计和使用介绍系列(9)— 一等公民“函数“爱炫巧

 

ShoneSharp语言(S#)的设计和使用介绍

系列(9)— 一等公民“函数“爱炫巧

作者:Shone

声明:原创文章欢迎转载,但请注明出处,https://www.cnblogs.com/ShoneSharp。

摘要: 匿名函数指函数定义体(即代码块)本身,使得函数成为所谓的“一等公民”,函数也可以像变量一样进行赋值定义、传递和使用。本文还介绍了函数式编程的各种技巧,如嵌套、递归、高阶、闭包等,站在函数式编程的顶峰,让你感叹“会当凌绝顶,一览众山小”!

软件: S#语言编辑解析运行器(ShoneSharp.13.6.exe),运行环境.NET4.0,单EXE直接运行,绿色软件无副作用。网盘链接https://pan.baidu.com/s/1nv1hmJn

 

在所有支持函数式编程的语言中,如LISP, Python, Ruby, JavaScript,函数的地位很高,都被视为“一等公民”。其背后的理论依据就是Lambada演算,业界早已证明其表达能力与传统过程式程序是等价的。函数式编程有很多技巧(即功能特性),高手能玩出花来。同样S#语言也支持这些特性,可能语法还更简洁灵活一点。

本文将全面展示“函数式编程”的各种特性或技巧,注意不是S#独有,其他函数式语言也都有。其他语言的学习者也可以进来看看,对比一下自己的语言实现方式,可以提高自己的函数式编程领悟力。

一、匿名函数

匿名函数(其他语言也叫lambada式、函数模板、元函数或自定义函数)指函数定义体(即代码块)本身,可以当作变量进行传递和使用,使得函数成为所谓的“一等公民”。实现方式上其实是通过指向函数入口的指针、地址或引用传递,在程序后面调用匿名函数时则把当前程序跳转到匿名函数体上执行,当然每次执行还要处理参数传递和堆栈进出。

S#匿名函数的表达方式很多种,其传递的语法解析树入口节点的引用。

1.1 func专用公式

func(形参序列 : 结果公式)

func专用基本公式建立形参变量堆栈,并对结果公式进行解析,但不求值,最终输出语法树入口节点的引用。其中每个形参定义一个变量名称,有多个变量时用逗号分割。func专用基本公式有局部变量堆栈,结果公式可以引用形参变量,还可以引用上层变量堆栈中的外部变量,若函数返回的是数组还可以采用省略写法。例如:

1
2
3
4
5
6
7
func(x: 2*x)          //单参数
 
func(x: x,2*x,3*x)    //单参数、返回数组
 
func(x: {x,2*x,3*x})  //单参数、返回列表
 
func(x,y: (x+y)/2)    //多参数

 

func专用基本公式可以把它赋值给变量后像系统函数那样调用(参见前一章节),其实还可以直接进行调用如下。

1
2
3
4
5
6
7
func(x: 2*x)(2)          //结果4
 
func(x: x,2*x,3*x)(2)    //结果[2,4,6]
 
func(x: {x,2*x,3*x})(2)  //结果{2,4,6}
 
func(x,y: (x+y)/2)(2,5)  //结果3.5

 

func(形参赋值序列 : 结果公式)

func专用扩展公式在func专用基本公式基础上扩展,差别对形参进行了初始化赋值。其中每个形参赋值写法为:变量名称=变量公式,有多个变量时用逗号分割。例如:

1
2
3
4
5
6
7
func(x=5: 2*x)           //单参数
 
func(x=5: x,2*x,3*x)     //单参数、返回数组
 
func(x=5: {x,2*x,3*x})   //单参数、返回列表
 
func(x=5,y=10: (x+y)/2)  //多参数

 

func扩展公式可以采用与func专用基本公式一样的调用方式如下。

1
2
3
4
5
6
7
func(x=5: 2*x)(2)            //结果4
 
func(x=5: x,2*x,3*x)(2)      //结果[2,4,6]
 
func(x=5: {x,2*x,3*x})(2)      //结果{2,4,6}
 
func(x=5,y=10: (x+y)/2)(2,5) //结果3.5

 

func扩展公式还可以采用指定实参赋值的调用方式,注意由于实参调用有名字,因此赋值顺序可以随意,也可以缺省,示例如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func(x=5: 2*x)(x=2)                //结果4
 
func(x=5: x,2*x,3*x)(x=2)          //结果[2,4,6]
 
func(x=5: {x,2*x,3*x})(x=2)        //结果{2,4,6}
 
func(x=5,y=10: (x+y)/2)(x=2,y=5)   //结果3.5
 
func(x=5,y=10: (x+y)/2)(y=5,x=2)   //结果3.5
 
func(x=5,y=10: (x+y)/2)(x=2)       //结果6
 
func(x=5,y=10:
(x+y)/2)(y=5)       //结果5

 

1.2 func演算公式

func(形参序列) => 结果公式

func演算基本公式与func专用基本公式相似,差别主要是写法上使用=>强调函数演算关系。注意1.2和1.3小节中的所有演算公式写法在函数返回数组或列表时不可以采用省略写法,必须显式使用数组或列表的构造符号。例如:

1
2
3
4
5
6
7
func(x)=>2*x          //单参数
 
func(x)=>[x,2*x,3*x]  //单参数、返回数组
 
func(x)=>{x,2*x,3*x}  //单参数、返回列表
 
func(x,y)=>(x+y)/2)   //多参数

 

func演算基本公式的调用如下,注意函数定义体必须也加上()进行隔离,否则代码含义不清。这种调用写法,学习JavaScript的同学应该很熟悉,其中如果函数定义嵌套的层次深了,()就会更多,也会有LISP的即视感,哈!

1
2
3
4
5
6
7
(func(x)=>2*x)(2)            //结果4
 
(func(x)=>[x,2*x,3*x])(2)    //结果[2,4,6]
 
(func(x)=>{x,2*x,3*x})(2)    //结果{2,4,6}
 
(func(x,y)=>(x+y)/2)(2,5)    //结果3.5

 

func(形参赋值序列) => 结果公式

func演算扩展公式与func专用扩展公式相似,差别也是写法上使用=>强调函数演算关系。例如:

1
2
3
4
5
6
7
func(x=5)=>2*x           //单参数
 
func(x=5)=>[x,2*x,3*x] //单参数、返回数组
 
func(x=5)=>{x,2*x,3*x} //单参数、返回列表
 
func(x=5,y=10)=>(x+y)/2)     //多参数

 

func演算扩展公式调用也要注意函数定义体必须也加上()进行隔离:

1
2
3
4
5
6
7
8
9
10
11
12
13
(func(x=5)=>2*x)(x=2)              //结果4
 
(func(x=5)=>[x,2*x,3*x])(x=2)      //结果[2,4,6]
 
(func(x=5)=>{x,2*x,3*x})(x=2)      //结果{2,4,6}
 
(func(x=5,y=10)=>(x+y)/2)(x=2,y=5) //结果3.5
 
(func(x=5,y=10)=>(x+y)/2)(y=5,x=2) //结果3.5
 
(func(x=5,y=10)=>(x+y)/2)(x=2)     //结果6
 
(func(x=5,y=10)=>(x+y)/2)(y=5)     //结果5

 

1.3 简化演算公式

单个形参 => 结果公式

单参简化演算公式与func演算基本公式相似,差别主要是写法上只支持单个参数,表达上更加精简,有C# lambada 单参数表达式的即视感。例如:

1
2
3
4
5
x=>2*x          //单参数
 
x=>[x,2*x,3*x]  //单参数、返回数组
 
x=>{x,2*x,3*x}  //单参数、返回列表

 

单参简化演算公式的调用也要注意函数定义体必须也加上()进行隔离:

1
2
3
4
5
(x=>2*x)(2)           //结果4
 
(x=>[x,2*x,3*x])(2)   //结果[2,4,6]
 
(x=>{x,2*x,3*x})(2)   //结果{2,4,6}

 

() => 结果公式

无参简化演算公式与单参简化演算公式相似,差别主要是用()表示该函数无形式参数,与C# lambada 无参数表达式也类似。例如:

1
()=>3*PI           //单参数

 

注意S#的无参数匿名函数只有这么一种写法。无参简化演算公式的调用也要注意函数定义体必须也加上()进行隔离:

1
(()=>3*PI)()          //结果9.42477796076938

 

二、函数定义

由于函数是一等公民了,因此函数通常也可以像变量一样进行赋值定义。S#有两种函数定义形式,但是其函数调用方法是一样的。

2.1 隐式函数定义

变量名称=匿名函数

这种写法最为灵活多变,因为匿名函数被看作普通数据,可以通过命名变量进行传递和使用,无论是传给其他函数作为实参调用,或是作为其他函数的返回值,都没有任何违和感觉。例如:

1
2
3
4
5
6
7
8
9
{
 
f = x => 2 * x  ,
 
g = f(5)  ,                 //直接调用,g结果10
 
h = { a = f , b = a(5) }    //变量传递后间接调用,b结果10
 
}

 

隐式函数定义可以使用在任何变量赋值公式中,而且函数变量的作用域还可以用专门语法进行修改。函数式编程的好多技巧就是通过匿名函数和隐式定义实现的,本文后面小节会专门论述。

2.2 显式函数定义(注:只能使用在eval专用公式中)

函数名称(形参序列) = 结果公式

函数定义基本公式与func专用基本公式相似,差别主要直接指定函数名称,并用=替代=>,最终输出时把语法解析树入口节点的引用赋值给指定名称的函数变量。注意2.2小节中的所有函数定义写法在函数返回数组或列表时也不可以采用省略写法,必须显式使用数组或列表的构造符号。例如:

1
2
3
4
5
6
7
8
9
eval(
 
f(x) = 2 * x  ,
 
g = f(5)  :              //直接调用,g结果10
 
{ a = f , b = a(5) }     //变量传递后间接调用,b结果10
 
)

 

从上面例子可以看出,显式函数定义写法更加简洁,但是函数变量的作用域只能按照默认的公开设置,不可以修改。

函数名称(形参赋值序列) = 结果公式

函数定义扩展公式在函数定义基本公式基础上扩展,差别对形参进行了初始化赋值。例如:

1
2
3
4
5
6
7
8
9
eval(
 
f(x=1) = 2 * x  ,
 
g = f(x=5)  :               //直接调用,g结果10
 
{ a = f , b = a(x=5) }      //变量传递后间接调用,b结果10
 
)

 

从上面例子可以看出,显式函数定义写法更加简洁,但是函数变量的作用域只函数名称() = 结果公式

无参函数定义公式更加简洁,直接使用()。例如:

1
2
3
4
5
6
7
8
9
eval(
 
f() = 2 * PI  ,
 
g = f()  :               //直接调用,g结果6.2831853071795862
 
{ a = f , b = a() }      //变量传递后间接调用,b结果6.2831853071795862
 
)

 

三、函数式编程技巧

3.1 函数嵌套调用

函数嵌套调用其实就是公式的嵌套,就是一个函数可以嵌套调用相同函数的另一次调用的结果作为参数。例如:

1
2
3
4
5
6
7
eval(
 
m(x) = x / 2  :    //定义对折函数
 
m(m(m(10)))       //三次对折后,结果1.25
 
)

 

函数嵌套调用使用上很简单,考验的是软件对函数变量传递的处理。

 3.2 递归函数调用

递归函数是指函数定义体内部也调用了函数自身,其实这就形成了循环的嵌套函数调用,因此通常必须明确指定终止条件,否则会进入死循环导致程序出错。

1
2
3
4
5
6
7
eval(
 
m(x) = if(x<2? x : m(x/2)) :   //定义递归对折函数
 
m(10)                          //递归对折后,结果1.25
 
)

 改为匿名递归函数更加简洁:

(x=> if(x<2? x : local(x/2)))(10)  //递归对折后,结果1.25

 

注意local指向局部堆栈所在的语法树节点,即匿名函数的语法树节点,这样就相当于指名调用了。别的语言要实现匿名递归比较复杂,也不直观。这就是S#所谓的数据即程序的特点。

3.3 函数参数是函数(高阶函数)

函数参数是函数其实就是一个函数变量作为另一个函数的参数进行传递。

1
2
3
4
5
6
7
8
9
eval(
 
m(x)=x/2 ,         //定义对折函数
 
f(array,y)=each(k@array:y(k)) :    //定义映射函数
 
f([10,20,30],m)    //对数组映射对折,结果[5,10,15]
 
)

 

上面是手工写,如果使用系统方法调用就会更加简洁,一句话搞定:

1
[10,20,30].Map(x=>x/2)      //映射元素,结果[5,10,15]

 

这里的.Map其实与其他C#的Convert,Python的map函数功能相同,可以接受函数作为参数。类似的函数还有:

1
2
3
[10,20,30].Filter(x=>x%4==0)  //过滤元素,结果[20]
 
[10,20,30].Count(x=>x%4==0)  //条件计数,结果1

 

列表也一样,例如:

1
2
3
4
5
{10,20,30}.Map(x=>x/2)      //映射元素,结果{5,10,15}
 
{10,20,30}.Filter(x=>x%4==0)  //过滤元素,结果{20}
 
{10,20,30}.Count(x=>x%4==0)  //条件计数,结果1

 

3.4 函数返回值是函数(闭包函数)

闭包是函数式编程的重要的语法结构,通常指能够读取其他函数内部变量的函数。很多编程语言只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上闭包是将函数内部和函数外部连接起来的桥梁。

1
2
3
4
5
6
7
eval(
 
m() = eval(n=2: x=>x/n) :  //注意n为局部变量,使用闭包函数访问
 
m()(10)     //调用闭包函数,打几折由内部说了算,结果5
 
)

 函数闭包的一种应用是可以将多个参数的函数进行拆分,拆成多个只有一个参数的函数,又称Curry,其实就是λ演算。比如:

func(x,y,z: x+y+z)(10,20,30)   //一次性传入3个参数,计算结果为60

Curry拆分写法如下:

func(x:func(y:func(z:x+y+z)))(10)(20)(30)   //拆分成3次调用,计算结果为60

或者更加接近C#写法:

(x=>(y=>(z=>x+y+z)))(10)(20)(30)

函数式编程能玩出花来,就是归功于函数可以作为返回并且进行匿名调用,其实你理解了其背后的思想,就会豁然开朗。

 

3.5 函数参数和返回值都是函数(高阶闭包函数)

 

复制代码
eval(

    g(f) = eval                    //传入双参函数

    (

        x = 10  ,

        y = 20  :

        n => n * f( x, y )         //返回单参函数

    )  :

    g(func(a,b)=>(a+b)/2)(10)      //结果150

)
复制代码

 

函数式编程的核心在于深入理解函数也是数据或变量的思想,只要你掌握其本质,那么在各种复杂使用场景中就可以驾轻就熟。

站在函数式编程的顶峰,你会感叹“会当凌绝顶,一览众山小”!

 

声明:原创文章欢迎转载,但请注明出处,https://www.cnblogs.com/ShoneSharp。

软件: S#语言编辑解析运行器(ShoneSharp.13.6.exe),运行环境.NET4.0,单EXE直接运行,绿色软件无副作用。网盘链接https://pan.baidu.com/s/1nv1hmJn

posted on   ShoneSharp  阅读(415)  评论(2编辑  收藏  举报

编辑推荐:
· .NET制作智能桌面机器人:结合BotSharp智能体框架开发语音交互
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· 一次Java后端服务间歇性响应慢的问题排查记录
阅读排行:
· 互联网不景气了那就玩玩嵌入式吧,用纯.NET开发并制作一个智能桌面机器人(四):结合BotSharp
· 一个基于 .NET 开源免费的异地组网和内网穿透工具
· 《HelloGitHub》第 108 期
· Windows桌面应用自动更新解决方案SharpUpdater5发布
· 我的家庭实验室服务器集群硬件清单
点击右上角即可分享
微信分享提示