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