函数中的类型是怎么工作的

函数中的类型是怎么工作的

理解类型标记



我们已经理解函数了,看看函数中的类型是怎么工作的,还有域和范围。这只是一个概述,这个"understandingF# types"系列会为你详细介绍。

首先,我们应该多理解类型符号一点。我们之后箭头符号用于域和范围,所以它总是像函数签名:

val functionName : domain -> range

看这些示例函数 :

let intToString x = sprintf "x is %i" x  // format int to string
let stringToInt x = System.Int32.Parse(x)

如果你在F#交互窗口中计算,你会看到这些签名:

val intToString : int -> string
val stringToInt : string -> int

这意味着:

  • intToString有一个int域映射向string的范围
  • stringToInt有一个string域映射向int的范围

基元类型

很多你可能期望的基元类型像string, int, float, bool, char, byte等等还有很多都衍生自.NET的类型系统。

看一些用基元类型的示例函数:

let intToFloat x = float x // "float" fn. converts ints to floats
let intToBool x = (x = 2)  // true if x equals 2
let stringToString x = x + " world"

他们的签名是:

val intToFloat : int -> float
val intToBool : int -> bool
val stringToString : string -> string

类型注解

在上个例子中,F#编译器准确的知道参数和返回值的类型。但它不总是这样。如果你用这些代码试一下,你会看过编译错误。

let stringLength x = x.Length
   => error FS0072: Lookup on object of indeterminate type

编译器不知道“x”是什么类型。因此它不知道“Length”是否是一个有效的方法。大多数情况下,给F#编译器一个类型注解就可以解决,让它知道用的是什么类型。在下边正确的版本中,我们指出"x"是一个string类型。

let stringLength (x:string) = x.Length       

括号里边的参数"x:string"很重要。如果没有它们,编译器会认为返回值也是一个string。别急,一个“open”(不带括号)冒号指定返回值类型,看看下边的例子。

let stringLengthAsInt (x:string) :int = x.Length 

我们指出"x"的参数类型是string,返回值类型是int。

函数作为参数

函数可以用其他函数作为参数,或者返回一个函数,这叫做higher-order function(高阶函数)缩写为HOF。他们用它作为一种抽象行为的方法,在F#中很普遍,大多数标准库也会使用。

思考这个函数evalWith5ThenAdd2,将一个函数作为参数,用5计算这个函数,然后加上2作为结果。

let evalWith5ThenAdd2 fn = fn 5 + 2     // same as fn(5) + 2

他们的签名是这样的:

val evalWith5ThenAdd2 : (int -> int) -> int

你可以看到域是"(int -> int)"范围是"int"。他们是什么意思呢?他的意思是输入参数不是一个简单值,而是一个函数,并且这个函数限制为ints到ints的映射,输出值不是一个函数,只是一个整数。

我们试一下:

let add1 x = x + 1      // define a function of type (int -> int)
evalWith5ThenAdd2 add1  // test it

执行后:

val add1 : int -> int
val it : int = 8

我们可以从"add1"的签名中看到,它也是一个从int映射到int的函数。所以对于函数evalWith5ThenAdd2它也是一个合法的参数。他的结果是8。

顺便说一下,"it"这个特殊的字用作表示最后被计算的,在这个例子中是指结果。它不是关键字,只是一个惯例。

看另外一个:

let times3 x = x * 3      // a function of type (int -> int)
evalWith5ThenAdd2 times3  // test it 

执行后:

val times3 : int -> int
val it : int = 17

我们可以从"times3"的签名中看到,它也是一个从int映射到int的函数。所以对于函数evalWith5ThenAdd2它也是一个合法的参数。他的结果是17。

注意输入对类型是敏感的,如果我们用float而不是 int,它就不能运行了,比如这样:

let times3float x = x * 3.0  // a function of type (float->float)
evalWith5ThenAdd2 times3float 

计算它将会有一个错误:

error FS0001: Type mismatch. Expecting a int -> int but
              given a float -> float    

意思是输入函数必须是int->int的函数 。

函数作为返回值

一个函数值可以是另一个函数的返回值。例如,下边的函数会生成一个使用输入值相加的"加法器"函数。

let adderGenerator numberToAdd = (+) numberToAdd

签名是这样的:

val adderGenerator : int -> (int -> int)

意思是这个生成的东西用一个int,创建一个从int映射到int的函数("加法器")。

我们看一下它是如何运行的:

let add1 = adderGenerator 1
let add2 = adderGenerator 2

创建两个加法器。第一个生成的函数为输入加1,第二个加2。注意签名和我们期望的一样。

val add1 : (int -> int)
val add2 : (int -> int)

现在我们正常的使用这些生成的函数。他们和明确定义的函数没区别。

add1 5    // val it : int = 6
add2 5    // val it : int = 7

为常量函数类型使用类型注释

在第一个例子中,我们有着一个函数:

let evalWith5ThenAdd2 fn = fn 5 +2
    => val evalWith5ThenAdd2 : (int -> int) -> int

在这个例子中,F#可以推断出"fn"是从ints到ints的映射,所以它的签名是int->int.

但是"fn"在下边的例子中的签名是什么?

let evalWith5 fn = fn 5

很显然,"fn"用一个int作为参数,那么它的返回值是什么类型?编译器无法告诉你。如果你想特别指出函数的类型,你可以像基元类型一样,为函数参数添加类型注解。

let evalWith5AsInt (fn:int->int) = fn 5
let evalWith5AsFloat (fn:int->float) = fn 5

另外,你也可以指出函数的返回类型:

let evalWith5AsString fn :string = fn 5

因为主函数返回一个string,所以"fn"也被约束为只能返回string,所以不用明确指定"fn"的类型。

"unit"类型

在编程中,我们有时候想让函数做一些事不返回值。思考下边定义的"printInt"。这个函数实际上不返回任何东西。它的作用就是在控制台打印一个字符串。

let printInt x = printf "x is %i" x  

那么,这个函数的签名是什么?

val printInt : int -> unit

什么是"unit"?

好吧,尽管一个函数没有什么返回值,它任然需要一个范围。在数学上,没有"void"的函数。每个函数必须有输出,因为这个函数是映射,映射就要有被映射的东西。

所以在F#中,返回一个特殊范围的函数叫做"unit"。这个域有一个值是()。你可以想象在C#中unit和()是"void(类型)"和"null(值)"。但是不像void/null,unit是一个真实的类型,()是一个真实的值。看这个,计算它:

let whatIsThis = ()

你会看过签名:

val whatIsThis : unit = ()

意思是whatIsThis是一个unit类型,它的值是()。

所以回到printfInt,我们现在明白它的签名了吧:

val printInt : int -> unit

签名说它有一个int的域,而且我们不用关心它映射到什么。

无参函数

现在我们理解"unit"类型了,我们能想象它在其他情况下的样子吗?我们创建一个可复用的"hello world"函数。当它没有输入也没有输出的时候,我们想象它的签名是unit->unit。看看是不是这样的:

let printHello = printf "hello world"        // print to console

结果是:

hello world
val printHello : unit = ()

和我们想象的不太一样。"Hello World"立即被打印,结果不是一个函数,是unit类型的一个简单值。像我们以前看到的,我们说它是简单值因为它有这样的签名:

val aName: type = constant

所以在这个例子中,我们看到printHelloWorld是一个简单值,它的值是()。我们以后不能叫做函数。

为什么printInt和printHelloWorld不同?在printInt中,只有当我们知道参数x的值时候,它的值才确定,所以它是一个函数。在printHelloWorld中,没有参数,所以右边是立即确定的。它只是返回()值,并且在控制台打印..

我们可以通过强制一个参数类型为unit去创建一个可复用的无参函数。像这样:

let printHelloFn () = printf "hello world"    // print to console

它的签名现在是:

val printHelloFn : unit -> unit

而且,调用它,我们不得不用一个()值作为参数,像这样:

printHelloFn ()

具有忽略函数的强制unit类型

在某种情况下,编译器需要个unit类型,并且抱怨。例如,下边的两个将会编译错误:

do 1+1     // => FS0020: This expression should have type ‘unit‘

let something =
  2+2      // => FS0020: This expression should have type ‘unit‘
  "hello"

有一个叫做ignore不需要任何参数,会返回一个unit类型的特殊函数对这样的情况有帮助。这些代码的正确版本应该是这样:

do (1+1 |> ignore)  // ok

let something =
  2+2 |> ignore     // ok
  "hello"

泛型

在很多情况下,一个函数的类型可以是任意类型,所以我们需要一个指定它的方式。在这种情况下,F#用.NET的泛型类型。

例如,下边的函数会把参数转换为字符串并且为了加点文本:

let onAStick x = x.ToString() + " on a stick"

不管参数是什么,所有的对象都知道ToString()。

它的签名是这样的:

val onAStick : ‘a -> string

‘a是个什么东西?这是F#指定泛型的方式,并且在编译时不知道是什么类型。"a"前边的单引号表示这是一个泛型。在C#中可以等效为:

string onAStick<a>();   

//or more idiomatically
string OnAStick<TObject>();   // F#‘s use of ‘a is like
                              // C#‘s "TObject" convention 

注意有泛型的F#依然是强类型的。它不是用一个object类型的参数。这种强类型是可取的,以便当函数组合在一起,依然可以保证类型安全。

这有一些用int,float,string的函数:

onAStick 22
onAStick 3.14159
onAStick "hello"

如果有两个泛型参数,编译器会给他们不同命名,‘a是第一个泛型参数,‘b是第二个泛型参数,等等。看下边这个示例:

let concatString x y = x.ToString() + y.ToString()

函数签名有两个泛型:‘a和‘b:

val concatString : ‘a -> ‘b -> string

另一方面,当只有一个参数时,编译器可以认出来是必要的。下边的例子中,x和y必须是同样的类型。

let isEqual x y = (x=y)

所以这个函数的签名,它们是相同的泛型:

val isEqual : ‘a -> ‘a -> bool 

泛型对于列表或者其他数据结构都很重要,我们在接下的例子中经常看到它们。

其他类型

目前讨论的类型都是基本类型。这些类型可以各种各样的组合成复杂类型。完整的讨论不得不等"another series"系列,同时这有一个简明的介绍,以便呢可以在函数签名中认出它们。

  • 元组类型。一对,三元组,等等。例如"("hello",1)"是一个由字符串和整数组成的元组。逗号是元组的特点,如果你在F#中看到逗号,你几乎可以确定这是元组的一部分。

在函数签名中,两种类型写起来像是乘法。在这个例子中,这个元组中像这样:

string * int      // ("hello", 1)
  • 集合类型。这些一般是列表,序列,数组。列表和数组是确定大小的,但是序列是无限的(幕后,序列和IEnumberable)。在函数签名中他们有自己的关键字:"list","seq",数组的是"[]"。
int list          // List type  e.g. [1;2;3]
string list       // List type  e.g. ["a";"b";"c"]
seq<int>          // Seq type   e.g. seq{1..10}
int []            // Array type e.g. [|1;2;3|]
  • 可选类型。这是一个会丢失的简单对象包装器。它有两种:Some和None。在函数签名中,它有自己的关键字"option":
int option        // Some(1)
  • 识别联合类型。这都是建立在其他类型的一组选择。我们在"why use F#"的例子中看到过。在函数签名中,他们引用类型的名称,没有关键字。
  • 记录类型。这些类似结构,数据库行,命名为插槽的列表。我们在"why use F#"的例子中看到过。在函数签名中,他们引用类型的名称,也没有关键字。

测试一些你理解的类型

你理解类型理解的有多好?为你准备了一些表达式-猜猜它们的签名。在交互窗口运行它们,看看你是否是对的。

let testA   = float 2
let testB x = float 2
let testC x = float 2 + x
let testD x = x.ToString().Length
let testE (x:float) = x.ToString().Length
let testF x = printfn "%s" x
let testG x = printfn "%f" x
let testH   = 2 * 2 |> ignore
let testI x = 2 * 2 |> ignore
let testJ (x:int) = 2 * 2 |> ignore
let testK   = "hello"
let testL() = "hello"
let testM x = x=x
let testN x = x 1          // hint: what kind of thing is x?
let testO x:string = x 1   // hint: what does :string modify? 


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

原文地址:http://fsharpforfunandprofit.com/posts/how-types-work-with-functions/

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

时间: 2024-08-07 17:01:26

函数中的类型是怎么工作的的相关文章

JavaScript中如何判断变量是数组、函数或是对象类型

数组 ECMAScript5中Array.isArray是原生的判断数组的方法,IE9及以上支持.考虑到兼容性,在没有此方法的浏览器中,可以使用 Object.prototype.toString.call(obj) === '[object Array]'替代. var isArray = Array.isArray || function(obj) {     return Object.prototype.toString.call(obj) === '[object Array]'; }

JavaScript中如何判断变量是数组、函数还是对象类型

数组 ECMAScript5中Array.isArray是原生的判断数组的方法,IE9及以上支持.考虑到兼容性,在没有此方法的浏览器中,可以使用Object.prototype.toString.call(obj) === '[object Array]'替代. var isArray = Array.isArray || function(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; } 函数 最

js中的类型和函数参数传递类型问题

js中的类型: 2大类型:原始类型和对象. 原始类型有 boolean.number.string这三个普通原始类型,还有null.undefined这俩特殊原始类型 对象嘛就多了,普通对象.内置对象.全局对象.函数.数组等. 函数参数传递类型: 对于原始类型,传递的是值,即复制一份传入函数,在函数内部修改不影响外部变量本身. 对于对象类型,传递的是地址,在函数内部修改对象时会导致外部变量发生变化. 注意这种情况!参考如下代码: var oMyObj = {name:"罗伯特"}; f

PHP中文件类型 文件属性 路径以及 文件相关的函数

一: 文件类型判断: 1.is_dir()  判断是不是目录 2.is_file() 判断是不是文件 3.is_executeable() 判断是不是可执行文件 4.is_readable()  判断是不是可读文件 5.is_writable() 判断是不是可写文件 6.is_link()  判断是不是快捷方式 二:文件属性: 1.file_exits()  文件是否存在 2.filesize() 文件大小 3.filectime()  文件创建时间 4.filemtime() 文件修改的时间

python函数中的参数类型

python函数中的参数 python的函数类型详解

c++中函数中变量内存分配以及返回指针、引用类型的思考

众所周知,我们在编程的时候经常会在函数中声明局部变量(包括普通类型的变量.指针.引用等等). 同时,为了满足程序功能的需要,函数的返回值也经常是指针类型或是引用类型,而这返回的指针或是引用也经常指向函数中我们自己声明的局部变量. 这样,程序在某些情况下就可能存在一定的问题.看似很简单的问题,通过仔细的分析,我们就能够更好的理解c++中内存分配和释放的问题. 好,废话不多说,我们进入正题.首先,简单介绍一下程序的内存区域的分配: 程序的内存分配 ①堆区(heap).这一部分主要是由程序开发人员自己

UNIX高级环境编程(3)Files And Directories - stat函数,文件类型,和各种ID

在前面的两篇,我们了解了IO操作的一些基本操作函数,包括open.read和write. 在本篇我们来学习一下文件系统的其他特性和一个文件的属性,涉及的函数功能包括: 查看文件的所有属性: 改变文件所有者: 改变文件权限: 操作文件夹. 我们还会了解一些文件系统相关的数据结构和符号链接(symbolic link). 1 函数stat.fstat.fstatat.lsat函数 #include <sys/stat.h> int stat(const char *restrict pathnam

C#中值类型和引用类型

本文将介绍C#类型系统中的值类型和引用类型,以及两者之间的一些区别.同时,还会介绍一下装箱和拆箱操作. 值类型和引用类型 首先,我们看看在C#中哪些类型是值类型,哪些类型是引用类型. 值类型: 基础数据类型(string类型除外):包括整型.浮点型.十进制型.布尔型. 整型(sbyte.byte.char.short.ushort.int.uint.long.ulong ) 浮点型(float 和 double ) 十进制型(decimal ) 布尔型(bool ) 结构类型(struct) 枚

【转】深入理解C++的动态绑定和静态绑定 &amp; 不要重定义虚函数中的默认参数

为了支持c++的多态性,才用了动态绑定和静态绑定.理解他们的区别有助于更好的理解多态性,以及在编程的过程中避免犯错误.需要理解四个名词:1.对象的静态类型:对象在声明时采用的类型.是在编译期确定的.2.对象的动态类型:目前所指对象的类型.是在运行期决定的.对象的动态类型可以更改,但是静态类型无法更改.关于对象的静态类型和动态类型,看一个示例: class B { } class C : public B { } class D : public B { } D* pD = new D();//p