F#学习之路(2) 深刻理解函数(下)
3、函数作用域
在F#中组织程序结构的方法有以下几种,使用类型 (type)来支持自定义数据类型,使用名称空间(namespace)来确保类型唯一,模块(module)则用来封装某一数据类型及其操作,而函数则封装了行为或业务逻辑。本文只简单介绍模块(module),其他内容将在下一主题中讨论。
函数是函数式语言中最基本的组成,是应用程序中具体业务逻辑的实现者,弄清其作用域非常必要。
在F#语言中,每一个源代码文件(本文仅指以.fs为扩展名的文件),其默认有一个以文件名首字母大写的模块名(module name),你在源代码中定义的常量、变量、类型、函数等其作用范围都在这个模块(module)中。
代码一:
F#使用 (* *)来多行注释,//单行注释,代码一中定义的常量c,变量v以及函数f其作用范围默认为Test 模块中(module),注意代码假定源代码文件名为test.fs
本文以下讨论作用域均在模块范围以内,关于如何自定义模块,命名空间,以及多模块、命名空间之间如何访问等内容在下一主题中讨论
在一个模块中,我把函数以外的作用域称为顶级作用域,函数体内称为局部作用域,通常在静态语言中作用域指的是定义域,但在F#中作用域因为lambda表达式,更具体地说因为闭包(closure)而引入了运行时访问域的概念(我暂且这样称呼他)。
F#中顶级作用域中定义的标识符名称必须是唯一的。而在函数体内定义的标识符名称却可以重复定义,且后者隐藏前者。
在下面的代码二中,函数f外,顶级作用域定义的标识符有c,v,包括f。这三个标识符名称必须唯一,否则会编译出错。
代码二:
但在函数体范围内却可以重复定义。
代码三:
你可以看到代码三中x被重复定义,但这样是允许的。
代码四:
上述的代码中有几点需要说明:
1、函数体内可以访问本模块的顶层标识(可访问在自身定义之前的标识,不能访问自身定义之后的顶层标识)
如果反注释函数f的最后一行表达式(x+y)*v+c+g会出错,编译器会报"值或构造器'g'未定义",只所以如此,是因为F#是从模块由上到下依次执行的(是不是因为需要兼容OCmal,还有交互式解释器的需要?)
2、函数体内定义的标识符会隐藏顶层标识
3、函数体内可通过定义同名的参数来隐藏参数
4、函数体内可改变顶级作用域中的变量
5、函数体内定义的同名标识后者覆盖前者
内部函数(函数内部定义的嵌套函数)和lambda(匿名函数)同样遵从上述规则,内部同名标识会隐藏外部或顶层标识,外部不能影响内部,函数体内后者覆盖前者
代码五:
代码五演示了在lambda中如何定义局部标识,内部的局部标识y会隐藏外部的标识
4、函数之泛型
F#使用类型推断技术让我们省却了声明类型的必要,他自动使用泛型,避免了我们大多数情况下书写泛型签名的必要
代码六:
fn函数声明了其参数a可以接受任何实现了seq<int>接口的类型。fn2函数是另一种等价于fn的函数,有时候你可能需要使用约束。
上面分别创建了列表,数组,以及序列。这三种类型我将专门在集合类型中讨论。
5、闭包、函数柯里化
代码七:
上面的代码定义了一个类型Operator,它是一个联合类型,也许称为类型构造器更适合,你可以认为是c语言中联合体的加强。它可以构造多种类型,但同一时刻只允许构造一种类型,这种特性与c语言中联合体相似,很显然它可以用来定义枚举类型,但更加强大的是,这种类型可以递归定义自身,因为非常适合定义树形结构。我将在以后的博客中专门使用一篇探讨自定义数据类型。
代码七中还演示了如何重新定义运算符
函数fn是一个二个参数的函数,其参数o为Operator类型,x为int类型,返回值为lambda表达式,在函数调用时你发现了一个问题,那就是二参数的函数如何使用三个值来调用,你要知道F#是强类型的语言。这就是currying柯里化的应用。在函数式语言中函数调用中参数不需要全部指定。在不全部指定的时候,发生了柯里化,其产生了一个以剩余参数为参数的函数。当补足全部参数时就发生调用过程。
代码八:
代码八中演示了currying柯里化的过程。
f1在调用fn时只提供了一个参数x=10,参数y,z未提供,所以f1是一个int->int->int的函数。
f2调用了f1,只提供了一个参数20,所以f2是一个int->int的函数
f3调用了fn,提供了二个参数,所以f3是一个int->int的函数。
柯里化是函数式编程中很重要的部分,函数式编程就是利用函数间组合来完成逻辑的,而柯里化使函数间组合变的简单容易。
总结:在纯函数语言中,函数是只能从参数中获取值,并且没有变量,而F#则可以直接访问函数外数据,并修改变量。而在我看来,使用函数式语言最有益的就是想更容易的很自己代码并发起来,而有副作用的函数使这一切变得不可能。所以我们应尽可能的让自已的函数无副作用,即只从函数参数中获取值,从而让自己的代码享受并发的好处。