有关函数式编程的系列文章的第三部分已经提出。 今天,我们将讨论这种范例的所有类型,并展示其用法示例。 有关原始类型,广义类型的更多信息,以及更多内容!

现在我们对功能有了一些了解,我们将看到类型如何与功能(例如域和范围)交互。 本文只是一篇评论。 为了更深入地浸入类型,有一系列“了解F#类型” 。
首先,我们需要更好地了解类型符号。 我们看到箭头符号“ ->
”将域和范围分开。 因此,函数签名始终如下所示:
val functionName : domain -> range
其他一些功能示例:
let intToString x = sprintf "x is %i" x // int string let stringToInt x = System.Int32.Parse(x)
如果在交互式窗口中执行此代码,则可以看到以下签名:
val intToString : int -> string val stringToInt : string -> int
他们的意思是:
intToString
具有类型为int
的域,该域映射到string
类型的范围。stringToInt
具有string
类型的域,该域映射到int
类型的范围。
原始类型
预期的基本类型为:字符串,整数,浮点型,布尔型,字符型,字节型,以及.NET类型系统的许多其他派生类。
带有基本类型的函数的更多示例:
let intToFloat x = float x // "float" - int float let intToBool x = (x = 2) // true x 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”为字符串。
let stringLength (x:string) = x.Length
x:string
参数周围的花括号很重要。 如果跳过它们,编译器将确定该字符串为返回值! 也就是说,如下面的示例所示,使用冒号表示返回值的类型。
let stringLengthAsInt (x:string) :int = x.Length
我们指出参数x
是一个字符串,返回值是一个整数。
函数类型作为参数
以其他函数为参数或返回一个函数的函数称为高阶函数 ( 高阶函数有时缩写为HOF)。 它们被用作抽象来尽可能地设置一般行为。 这种功能在F#中非常常见,大多数标准库都使用它们。
考虑函数evalWith5ThenAdd2
,该函数将一个函数作为参数,然后从5计算该函数并将2加到结果中:
let evalWith5ThenAdd2 fn = fn 5 + 2 // , fn(5) + 2
该函数的签名如下所示:
val evalWith5ThenAdd2 : (int -> int) -> int
您可以看到domain是(int->int)
,range是int
。 这是什么意思? 这意味着输入参数不是一个简单的值,而是从int
到int
许多函数中的一个函数。 输出值不是函数,而只是int
。
让我们尝试:
let add1 x = x + 1 // - (int -> int) evalWith5ThenAdd2 add1 //
并获得:
val add1 : int -> int val it : int = 8
正如我们从签名中看到的,“ add1
”是一个将int
映射到int
的函数。 它是evalWith5ThenAdd2
的有效参数,其结果是8。
顺便说一句,特殊词“ it
”用于表示最后的计算值,在这种情况下,这是我们等待的结果。 这不是关键字,只是命名约定。
另一种情况:
let times3 x = x * 3 // - (int -> int) evalWith5ThenAdd2 times3 //
给出:
val times3 : int -> int val it : int = 17
从签名可以看出,“ times3
”也是一个将int
映射到int
的函数。 这也是evalWith5ThenAdd2
的有效参数。 计算结果为17。
请注意,输入数据是类型敏感的。 如果传递的函数使用float
而不是int
,则将无济于事。 例如,如果我们有:
let times3float x = x * 3.0 // - (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
并创建一个将ints
映射到ints
的函数(“加法器”)。 让我们看看它是如何工作的:
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
”将int
转换为int
,因此其签名为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
”。
输入“单位”
在编程过程中,有时我们希望函数执行某些操作而不返回任何内容。 考虑函数“ printInt
”。 该函数实际上什么也不返回。 它只是将字符串打印到控制台,这是执行的副作用。
let printInt x = printf "x is %i" x //
她的签名是什么?
val printInt : int -> unit
什么是“ unit
”?
即使该函数不返回值,它仍然需要范围。 在数学世界中没有“无效”函数。 每个函数必须返回某些内容,因为该函数是一个映射,并且该映射必须显示某些内容!

因此,在F#中,像这样的函数会返回一种特殊的结果,称为“ unit
”。 它仅包含一个值,用“ ()
”表示。 您可能会认为unit
和()
分别类似于C#中的“ void”和“ null”。 但是与它们不同的是, unit
是实际类型,而()
实际值。 要验证这一点,只需执行以下操作:
let whatIsThis = ()
将收到以下签名:
val whatIsThis : unit = ()
表示标签“ whatIsThis
”的类型为unit
并与值()
关联。
现在,回到“ printInt
”签名,我们可以理解该条目的含义:
val printInt : int -> unit
此签名表明printInt
具有int
的域,这意味着我们不感兴趣。
没有参数的功能
现在我们了解了unit
,是否可以预测它在不同上下文中的出现? 例如,尝试创建可重用的函数“ hello world”。 由于没有输入或输出,因此可以期望签名unit -> unit
。 让我们看看:
let printHello = printf "hello world" //
结果:
hello world val printHello : unit = ()
不完全符合我们的预期。 立即输出“ Hello world”,结果不是函数,而是类型unit的简单值。 我们可以说这是一个简单的值,因为正如我们前面所看到的,它具有以下形式的签名:
val aName: type = constant
在此示例中,我们看到printHello
一个简单的value ()
。 这不是我们以后可以调用的函数。
printInt
和printHello
什么printHello
? 在printInt
的情况下,直到我们知道参数x
printInt
值才能确定该值,因此定义是一个函数。 对于printHello
,没有参数,因此可以在右侧定义右侧。 并且它等于()
并且具有向控制台输出的形式的副作用。
您可以创建一个没有参数的真正的可重用函数,从而强制定义具有一个unit
参数:
let printHelloFn () = printf "hello world" //
现在她的签名等于:
val printHelloFn : unit -> unit
并调用它,我们必须将()
作为参数传递:
printHelloFn ()
使用忽略功能加强单位类型
在某些情况下,编译器需要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>(); // string OnAStick<TObject>(); // F#- 'a // C#'- "TObject"
应该理解,即使使用泛型类型,此F#函数仍具有强类型。 它不接受Object
类型的参数。 强类型输入是好的,因为它使您可以在编写函数时保持其类型安全。
相同的函数用于int
, float
和string
。
onAStick 22 onAStick 3.14159 onAStick "hello"
如果有两个通用参数,则编译器将为它们提供两个不同的名称: 'a
代表第一个, 'b
代表第二个,依此类推。” 例如:
let concatString xy = x.ToString() + y.ToString()
此签名中将有两种通用类型: 'a
和'b
:
val concatString : 'a -> 'b -> string
另一方面,编译器识别何时仅需要一个通用类型。 在以下示例中, x
和y
必须具有相同的类型:
let isEqual xy = (x=y)
因此,函数签名对于两个参数具有相同的通用类型:
val isEqual : 'a -> 'a -> bool
当涉及到列表和其他抽象结构时,通用参数也非常重要,在以下示例中我们将看到很多。
其他种类
到目前为止,仅讨论了基本类型。 这些类型可以通过各种方式组合为更复杂的类型。 它们的完整分析将在另一个系列的后面部分中,但与此同时,在这里我们将对其进行简要分析,以便您可以在函数签名中识别它们。
- 元组 这是由其他类型组成的一对,三重等。 例如,
("hello", 1)
是一个基于string
和int
的元组。 逗号是元组的标志;如果在F#中的某个地方看到逗号,则几乎可以保证它是元组的一部分。
在函数签名中,元组被写为所涉及的两种类型的“产品”。 在这种情况下,元组将是以下类型:
string * int // ("hello", 1)
- 集合 。 最常见的是列表(列表),序列(序列)和数组。 列表和数组的大小是固定的,而序列可能是无限的(在幕后,序列是相同的
IEnumrable
)。 在函数签名中,它们具有自己的关键字:“ list
”,“ seq
”和“ []
”表示数组。
int list // List type [1;2;3] string list // List type ["a";"b";"c"] seq<int> // Seq type seq{1..10} int [] // Array type [|1;2;3|]
- 选项(可选类型) 。 这是对可能丢失的对象的简单包装。 有两个选项:“
Some
(当值存在时)和“ None
(当值不存在时)。 在函数签名中,它们具有自己的关键字“ option
”:
int option // Some 1
- 标记的协会(有区别的工会) 。 它们由其他类型的许多变体构建而成。 我们在“为什么使用F#?”中看到了一些示例。 。 在函数签名中,它们由类型名称引用;它们没有特殊的关键字。
- 记录类型(记录) 。 类型,如数据库结构或行,一组命名值。 我们还在“为什么使用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 // : x? let testO x:string = x 1 // : :string ?
其他资源
F#的教程很多,包括那些具有C#或Java经验的人的材料。 当您深入了解F#时,以下链接可能会很有用:
还介绍了其他几种开始学习F#的方法 。
最后,F#社区非常适合初学者。 在Slack上,由F#Software Foundation支持的聊天非常活跃,您可以自由加入初学者室。 我们强烈建议您这样做!
不要忘记访问俄语社区F#的网站 ! 如果您对学习语言有任何疑问,我们将很乐意在聊天室中讨论这些问题:
关于翻译作者
由@kleidemos翻译
在F#开发人员的俄语社区的努力下进行了翻译和编辑更改。 我们也感谢@schvepsss和@shwars为本文准备发表。