函数结合性和组成

函数结合性和组成

从已有的函数中构造函数


函数结合性

如果在一行中有一个函数链,他们应该如何组合?

例如,这是什么意思?

let F x y z = x y z

它是不是意思是z作为函数y的参数,之后用y的结果作为函数x参数?

在这个例子中,像这样:

let F x y z = x (y z)

或者意思是y作为函数x的参数,之后z作为函数x的返回函数的参数?在这个例子中,像这样:

let F x y z = (x y) 

答案是后者。函数应用是左联的。就是,执行x y z和执行(x y) z是一样的。还有执行 w x y z和执行((w x) y) z是一样的。这不奇怪。我们已经知道分部应用是如何工作的了。如果你想象x是一个两个参数的函数,第一个参数的分部应用的结果就是(x y) z,接着传递一个参数z到这个中间函数中。

如果你想用右联,你可以用括号指明,或者你也可以用管道。这三种格式都是相同的:

let F x y z = x (y z)
let F x y z = y z |> x    // using forward pipe
let F x y z = x <| y z    // using backward pipe

作为一个测试,不执行它们的情况下计算出来它们的签名。

函数组合

我们之前已经很多次提到函数组合,但是它实际上是什么意思呢?它一开始看起来很吓人,但实际上很简单。

已知你有一个从类型"T1"映射到类型"T2"的函数"f"而且你有一个从类型"T2"映射到类型"T3"的函数"g"。之后你可以新建一个从类型"T1"映射到类型"T3"函数把"f"的输出和"g"的输入联系起来。

这有一个例子:

let f (x:int) = float x * 3.0  // f is int->float
let g (x:float) = x > 4.0      // g is float->bool

我们可以新建一个拿"f"的输出作为"g"的输入的函数。

let h (x:int) =
    let y = f(x)
    g(y)                   // return output of g

更加紧凑的写法是:

let h (x:int) = g ( f(x) ) // h is int->bool

//test
h 1
h 2

到目前为止,如此简单。我们可以定一个叫做"组合"的新函数,有趣的是给定函数"f"和"g",用一个甚至不知道他们签名的方式就可以组合他们。

let compose f g x = g ( f(x) ) 

如果你执行它,你可以看到编译器会正确的推导出"f"是一个从泛型‘a到泛型‘b的函数,而且函数"g"被限制为输入类型是‘b。全部的签名是这样的:

val compose : (‘a -> ‘b) -> (‘b -> ‘c) -> ‘a -> ‘c

(注意泛型组合操作只是有可能的因为函数只能有一个输入和一个输出。这种方法在非函数式语言中是不可行的。)

正如我们所看到的,组合的实际定义是使用“>>”符号。

let (>>) f g x = g ( f(x) )

根据这个定义,我们可以从已经存在的函数中构建一个新函数。

let add1 x = x + 1
let times2 x = x * 2
let add1Times2 x = (>>) add1 times2 x

//test
add1Times2 3

这种直接的方式很杂乱。我们可以做一些事使它更便于使用和理解。

首先,我们抛开参数"x"以至于组合操作符返回一个分部应用。

let add1Times2 = (>>) add1 times2

现在我们有一个两个东西组成的操作,所以我们可以把操作符放在中间。

let add1Times2 = add1 >> times2

就是这样。使用组合操作符使代码更加干净也更加直观。

let add1 x = x + 1
let times2 x = x * 2

//old style
let add1Times2 x = times2(add1 x)

//new style
let add1Times2 = add1 >> times2

实践组合操作符

组合操作符(像所有的中缀操作符)优先级低于正常的函数应用。这意味着组合函数中的函数不需要用括号。

例如,如果"add"和"times"函数有一个附加的参数,它可以在组合期间被传递。

let add n x = x + n
let times n x = x * n
let add1Times2 = add 1 >> times 2
let add5Times3 = add 5 >> times 3

//test
add5Times3 1

只要输入和输出匹配,任何一种值都可以参与到函数。例如,考虑下边的,执行一个函数两次:

let twice f = f >> f    //signature is (‘a -> ‘a) -> (‘a -> ‘a)

注意编译器推断出输入和输出必须都是同样的类型。

现在考虑像"+"这样的函数。像我们之前看到的,输入是个int,但是输出实际上是一个分部应用函数int->int。"+"的输出也可以是"twice"的输入。所以我们可以这样写:

let add1 = (+) 1           // signature is (int -> int)
let add1Twice = twice add1 // signature is also (int -> int)

//test
add1Twice 9

另一面方面,我们不能这样写:

let addThenMultiply = (+) >> (*)

因为"*"的输入必须是int值,不是一个int->int函数(加法的输出是)。

但是如果调整它以至于它的输出而是一个int值,之后它就可以运行:

let add1ThenMultiply = (+) 1 >> (*)
// (+) 1 has signature (int -> int) and output is an ‘int‘

//test
add1ThenMultiply 2 7 

如果需要,组合也可以使用"<<"向后这样做。

let times2Add1 = add 1 << times 2
times2Add1 3

反转组合可以使代码看起来更像英语那样。例如,这个简单的例子:

let myList = []
myList |> List.isEmpty |> not    // straight pipeline

myList |> (not << List.isEmpty)  // using reverse composition 

组合对比管道

在这一节,你可能惊讶组合操作符和管道操作符的不同,尽管它们看起来很相似。

首先,我们再看一下管道操作符的定义:

let (|>) x f = f x

所有这些允许我们把函数的参数放在前边而不是后边。就是这样。如果函数有多个参数,输入将会是最后一个参数。这是我们之前用到的例子:

let doSomething x y z = x+y+z
doSomething 1 2 3       // all parameters after function
3 |> doSomething 1 2    // last parameter piped in

组合不是同样的东西也不会是管道的代替品。在下边的例子中数字3甚至不是一个函数,所以它的"输出"不会进入"doSomething":

3 >> doSomething 1 2     // not allowed
// f >> g is the same as  g(f(x)) so rewriting it we have:
doSomething 1 2 ( 3(x) ) // implies 3 should be a function!
// error FS0001: This expression was expected to have type ‘a->‘b
//               but here has type int    

编译器抱怨"3"应该是‘a->‘b的顺序。

对比组合的定义,它需要三个参数,前两个必须是函数。

let (>>) f g x = g ( f(x) )

let add n x = x + n
let times n x = x * n
let add1Times2 = add 1 >> times 2

尝试使用管道也不可行。在下边的例子中,"add1"是一个类型是int->int的(分部)函数,不能用作"times 2"的第二个参数。

let add1Times2 = add 1 |> times 2   // not allowed
// x |> f is the same as  f(x) so rewriting it we have:
let add1Times2 = times 2 (add 1)    // add1 should be an int
// error FS0001: Type mismatch. ‘int -> int‘ does not match ‘int‘

编译器抱怨"times 2"应该用一个int->int类型的参数,而不是,(int->int)->‘a类型。



翻译有误,请指正,谢谢!

原文地址:http://fsharpforfunandprofit.com/posts/function-composition/

翻译目录传送门:http://www.cnblogs.com/JayWist/p/5837982.html

时间: 2024-11-01 21:15:26

函数结合性和组成的相关文章

c指针 --笔记2返回指针值的函数

返回指针值的函数 一般带回指针值的函数,定义形式为: int *a (int x, int y); 看这个经典案例: #include <stdio.h> int main(int argc, char const *argv[]) { double score[][4] = {{60.0, 70.0, 80.5, 20.1}, {60.0, 70.0, 80.5, 21.1}, {60.0, 70.0, 80.5, 22.1}}; double *search(double(*pointer

[c语言]运算符的优先级与结合性

c语言中运算符的优先级和结合性常常被人混淆一谈,本文目的在于简单谈谈两者的区别.本文举几个简单的例子说明,这些运算符也特别常用. 首先要明白的是:优先级决定表达式中各种不同的运算符起作用的优先次序:而结合性则在相邻的运算符的具有同等优先级时,决定表达式的结合方向. [赋值运算符“=”] 对于赋值运算符来说,常会用到的是连续赋值的表达式.比如“a=b=c”. 这里的变量b的两边都是赋值运算,优先级当然是相同的,那么应该怎么理解这个表达式呢?我们知道,赋值表达式具有“向右结合”的特性,这就表示这个表

【C知识点】C 运算符优先级和结合性y

1.C语言的运算符优先级有15个,如下图所示. 优先级 运算符 名称或含义 使用形式 结合方向 说明 1 [] 数组下标 数组名[常量表达式] 左到右 调用函数,数组 结构体成员选择符 () 圆括号 (表达式)/函数名(形参表)   . 成员选择(对象) 对象.成员名   -> 成员选择(指针) 对象指针->成员名   2 - 负号运算符 -表达式 右到左 单目运算符 优先级别仅此前者 (类型) 强制类型转换 (数据类型)表达式   ++ 自增运算符 ++变量名/变量名++ -- 自减运算符

说说C语言运算符的“优先级”与“结合性”

论坛和博客上常常看到关于C语言中运算符的迷惑,甚至是错误的解读.这样的迷惑或解读大都发生在表达式中存在着较为复杂的副作用时.但从本质上看,仍然是概念理解上的偏差.本文试图通过对三个典型表达式的分析,集中说说运算符的优先级.结合性方面的问题,同时说明它们跟求值过程之间存在的区别与联系. 优先级决定表达式中各种不同的运算符起作用的优先次序,而结合性则在相邻的运算符的具有同等优先级时,决定表达式的结合方向. (一)a = b = c;关于优先级与结合性的经典示例之一就是上面这个"连续赋值"表

C/C++函数指针(typedef简化定义)

学习要点:        1,函数地址的一般定义和typedef简化定义;        2,函数地址的获取;        3,A函数地址作为B函数参数的传递;    函数存放在内存的代码区域内,它们同样有地址.如果我们有一个int test(int a)的函数,那么,它的地址就是函数的名字,这一点如同数组一样,数组的名字就是数组的起始地址.    定义一个指向函数的指针用如下的形式,以上面的test()为例:    int (*fp)(int a);//这里就定义了一个指向函数(这个函数的参

C++ Primer 学习笔记_26_操作符重载与转换(1)--可重载/不可重载的操作符、成员函数方式重载、友元函数方式重载

C++ Primer 学习笔记_26_操作符重载与转换(1)--可重载/不可重载的操作符.成员函数方式重载.友元函数方式重载 引言: 明智地使用操作符重载可以使类类型的使用像内置类型一样直观! 一.重载的操作符名 像任何其他函数一样,操作符重载函数有一个返回值和一个形参表.形参表必须具有操作符数目相同的形参.比如赋值时二元运算,所以该操作符函数有两个参数:第一个形参对应着左操作数,第二个形参对应右操作数. 大多数操作符可以定义为成员函数或非成员函数.当操作符为成员函数时,它的第一个操作数隐式绑定

运算符、优先级、结合性

分类: C/C++ C语言语句分为5类: 1.   表达式语句 2.   函数调用语句 3.   控制语句 4.   复合语句 5.   空语句 表达式语句:表达式+分号: 函数调用语句:函数名+实际参数+分号:格式一般为:函数名(参数表): 控制语句: 条件判断语句 if语句,switch语句 循环执行语句 do while语句,while语句,for语句 转向语句 break语句,goto语句,continue语句,return语句 复合语句: 多个语句用{}括起来,组成复合语句,其中每条语

编写高质量代码——运算符重载,是成员函数还是友元函数

一.运算符重载的四项基本原则: ▍不可臆造运算符. ▍运算符原有操作数的个数.优先级和结合性不能改变. ▍操作数中至少一个是自定义类型. ▍保持运算符的自然含义. ============================== 二.运算符重载的两种形式: ▍成员函数形式(隐含一个参数 this 指针): 1)双目运算符:参数一个 2)单目运算符:不能显示的声明参数 ▍友元函数形式(不存在隐含的参数 this 指针) 1)双目运算符:两个参数 2)单目运算符:一个参数 ===============

sizeof既是关键字,又是运算符(操作符),但不是函数!

sizeof是关键字吗 sizeof是关键字,这一点毋庸置疑.你不能将sizeof定义为任何标识符.查看C语言标准文档里的说明: sizeof是运算符(操作符)吗 C语言中,sizeof是运算符(操作符),而且是唯一一个以单词形式出现的运算符,它用来计算存放某一个量需要占用多少字节,它的结合性是从右到左.查看C语言标准文档里的说明: sizeof是函数吗 sizeof不是函数.产生这样的疑问主要是因为有时候sizeof的外在表现确实有点类似函数,比如:i = sizeof(int);这样的式子,