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

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

理解类型标记


我们已经理解函数了,看看函数中的类型是怎么工作的,还有域和范围。这只是一个概述,这个"understanding F# 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"。他们是什么意思呢?他的意思是输入参数不是一个简单值,而是一个函数,并且这个函数限制为intsints的映射,输出值不是一个函数,只是一个整数。

我们试一下:

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"是从intsints的映射,所以它的签名是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是一个简单值,它的值是()。我们以后不能叫做函数。

为什么printIntprintHelloWorld不同?在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类型的参数。这种强类型是可取的,以便当函数组合在一起,依然可以保证类型安全。

这有一些用intfloat,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

 

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

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|]

 

  •     可选类型。这是一个会丢失的简单对象包装器。它有两种:SomeNone。在函数签名中,它有自己的关键字"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

posted @ 2016-09-08 22:33  JayWist  阅读(713)  评论(0编辑  收藏  举报