F# 之旅(上)

时间:2022-04-23 11:30:56

一、写在前面的话

解答一下在上一篇文章《在Visual Studio中入门F#》中有人的提问,

  1. 问:是准备写 F# 系列吗?

      答:当然不是,本人也是刚刚学习 F#,只是翻译微软官方的文档,但是我会尽力翻译更多的文章。

  2. 问:你们的项目使用F#写的吗?

      答:本人大三学生,也不是什么大佬,兴趣而已。

二、在这篇文章中

  • 怎样运行示例代码
  • 函数和模块
  • 数字、布尔值和字符串
  • 元组
  • 管线和组成
  • 列表、数组和序列

学习 F# 最好的方式是读写 F# 代码。本文将介绍 F# 语言的一些主要功能,并给您一些可以在您计算机上执行的代码片段。

F# 中有两个重要概念:函数和类型。本教程将强调 F# 的这两个特点。

三、原文链接

Tour of F#

备注,原文较长,本人分为《F# 之旅》上下两部分翻译。

四、开始

1. 怎样运行示例代码
        执行这些示例代码最快的方式是使用 F# Interactive。仅仅通过拷贝/粘贴示例代码就能运行它们了。当然,您也可以在 Visual Studio 中建立一个控制台应用程序来编译并运行它们。

2. 函数或模块
        组织在模块中的函数是任何 F# 程序最基本的部分。函数执行根据输入以生成输出的工作,并使用模块进行组织,这是在 F# 中组织事物的主要方式。函数使用“let 绑定”关键字定义, 需要一个名称并定义参数。

 module BasicFunctions = 

     /// 您使用“let”定义一个函数。这个函数接收一个整数类型的参数,返回一个整数。
let sampleFunction1 x = x*x + /// 使用函数,使用“let”命名函数的返回值。
/// 变量的类型由函数的返回值推断而出。
let result1 = sampleFunction1 // 本行使用“%d”以 int 类型格式输出结果。这是类型安全的。
// 如果“result1”不是“int”类型,那么该行将无法编译成功。
printfn "The result of squaring the integer 4573 and adding 3 is %d" result1 /// 当有需要时,可使用“(argument:type)”注明参数的类型。括号是必需的。
let sampleFunction2 (x:int) = *x*x - x/ + let result2 = sampleFunction2 ( + )
printfn "The result of applying the 2nd sample function to (7 + 4) is %d" result2 /// 条件表达式由 if/then/elif/else 构成。
///
/// 注意 F# 使用空格缩进语法,与 Python 相似。
let sampleFunction3 x =
if x < 100.0 then
2.0*x*x - x/5.0 + 3.0
else
2.0*x*x + x/5.0 - 37.0 let result3 = sampleFunction3 (6.5 + 4.5) // 本行使用“%f”将结果以 float 类型格式输出。同上述的“%d”,这是类型安全的。
printfn "The result of applying the 2nd sample function to (6.5 + 4.5) is %f" result3

“let 绑定”关键字同样可以将值绑定到名称上,与其他语言中的变量类似。默认情况下,绑定后便不可变更,这意味值或函数绑定到名称上后,就不能在更改了。这与其他语言中的变量不同,它们是可变的,也就是说,它们的值可以在任何时间点上更改。如果需要可变的绑定,可以使用“let mutable”语法。

 module Immutability =

     /// 使用“let”将一个值绑定到名称上,它不可改变。
///
/// 代码第二行编译失败,因为“number”是不可变的。
/// 重定义“number”为一个不同的值,在 F# 中是不允许的。
let number =
// let number = 3 /// 一个可变的绑定。“otherNumber”能够改变值。
let mutable otherNumber = printfn "'otherNumber' is %d" otherNumber // 当需要改变值时,使用“<-”分配一个新值。
//
// 注意,“=”与此不同,“=”用于判断相等。
otherNumber <- otherNumber + printfn "'otherNumber' changed to be %d" otherNumber

3. 数字、布尔值和字符串

作为一门 .NET 语言,F# 同样支持 .NET 底层的基本类型。

下面是在 F# 中表示各种数值类型的方式:

 module IntegersAndNumbers = 

     /// 这是整数示例。
let sampleInteger = /// 这是浮点数示例。
let sampleDouble = 4.1 /// 这里使用一些运算符计算得到一个新的数字。数字类型使用
/// “int”,“double”等函数进行转换。
let sampleInteger2 = (sampleInteger/ + - ) * + int sampleDouble /// 这是一个从0到99的数字列表。
let sampleNumbers = [ .. ] /// 这是一个由0到99的数字和它们的平方数构成的元组所形成的列表。
let sampleTableOfSquares = [ for i in .. -> (i, i*i) ] // 接下来的一行输出这个包含许多元组的列表,使用“%A”进行泛型化输出。
printfn "The table of squares from 0 to 99 is:\n%A" sampleTableOfSquares

布尔值像这样执行基础的条件判断逻辑:

 module Booleans =

     /// 布尔类型的值为“true”和“fales”。
let boolean1 = true
let boolean2 = false /// 对布尔类型进行操作的运算符有“not”,“&&”和“||”。
let boolean3 = not boolean1 && (boolean2 || false) // 本行使用“%d”输出布尔类型值。这是类型安全的。
printfn "The expression 'not boolean1 && (boolean2 || false)' is %b" boolean3

下面介绍基本的字符串操作:

 module StringManipulation = 

     /// 字符串需要使用双引号。
let string1 = "Hello"
let string2 = "world" /// 字符串同样可以使用“@”创建逐字字符串。
/// 这将忽略“\”,“\n”,“\t”等转义字符。
let string3 = @"C:\Program Files\" /// 字符串文本也可以使用三重引号。
let string4 = """The computer said "hello world" when I told it to!""" /// 字符串通常使用“+”运算符进行连接。
let helloWorld = string1 + " " + string2 // 本行使用“%d”输出一个字符串变量。这是类型安全的。
printfn "%s" helloWorld /// 子字符串使用索引器表示。本行提取前7个字符作为子字符串。
/// 注意,与许多编程语言相同,字符串在 F# 中从0开始索引。
printfn "%s" substring

4. 元组

元组在 F# 中处于很重要的位置。它是一组未命名的,但有序的值,它们整体就能当作一个值。可以将它们理解为由其他值聚合而成的值。它们有许多的用途,例如方便函数返回多个值,方便特殊值组织在一起。

 module Tuples =

     /// 一个由整数构成的元组示例。tuple1 的类型是 int*int*int
let tuple1 = (, , ) /// 一个交换元组中两个值顺序的函数。
///
/// F# 类型推断将自动泛化函数,意味着可以在任何类型下工作。
let swapElems (a, b) = (b, a) printfn "The result of swapping (1, 2) is %A" (swapElems (,)) /// 一个由一个整数、一个字符串和一个双精度浮点数组成的元组。tuple2 的类型是 int*string*float
let tuple2 = (, "fred", 3.1415) printfn "tuple1: %A\ttuple2: %A" tuple1 tuple2

在 F# 4.1 中,您还可以使用 struct 关键字将一个元组定义为结构体元组。这些同样可以与C# 7/Visual Basic 15 中的结构体元组进行互操作:

 /// 元组通常是对象,但是它们也可以表示为结构体。
///
/// 它们完全可以与 C# 和 Visual Basic.NET 中的结构体元组进行互操作。
/// 结构体元组不能隐式转换为对象元组 (通常称为引用元组)。
///
/// 因为上述原因,下面的第二行将无法编译。
let sampleStructTuple = struct (, )
//let thisWillNotCompile: (int*int) = struct (1, 2) // 您可以这样做。
let convertFromStructTuple (struct(a, b)) = (a, b)
let convertToStructTuple (a, b) = struct(a, b) printfn "Struct Tuple: %A\nReference tuple made from the Struct Tuple: %A" sampleStructTuple (sampleStructTuple |> convertFromStructTuple)

您需要特别注意,因为结构体元组是值类型,您不能将它们隐式转换为引用元组,反之亦然。引用元组和结构体元组间必需进行显示转换。

5. 管线和组成

管道运算符(| >, <|, | |> |>, <| | |) 和组合运算符  (>> 和 <<) 在 F# 中被广泛用于数据处理。这些运算符是函数,它们允许您以灵活的方式来创建函数的“管线”。下面的示例将带您浏览如何利用这些运算符来构建一个简单的功能管线。

 module PipelinesAndComposition =

     /// 计算 x 的平方。
let square x = x * x /// 计算 x + 1。
let addOne x = x + /// 测试 x 是否是奇数。
let isOdd x = x % <> /// 一个包含5个数字的列表。
let numbers = [ ; ; ; ; ] /// 传入一个数字列表,它将筛选出偶数,
/// 再计算结果的平方,然后加1。
let squareOddValuesAndAddOne values =
let odds = List.filter isOdd values
let squares = List.map square odds
let result = List.map addOne squares
result printfn "processing %A through 'squareOddValuesAndAddOne' produces: %A" numbers (squareOddValuesAndAddOne numbers) /// 修改 “squareOddValuesAndAddOne” 更短的方法是将每个过程中产生的中间
/// 结果嵌套到函数调用本身。
///
/// 这样使函数变得更短,但是这样很难查看函数的执行顺序。
let squareOddValuesAndAddOneNested values =
List.map addOne (List.map square (List.filter isOdd values)) printfn "processing %A through 'squareOddValuesAndAddOneNested' produces: %A" numbers (squareOddValuesAndAddOneNested numbers) /// 一个更好的方式去编写 “squareOddValuesAndAddOne” 函数,那就是使用 F#
/// 的管道运算符。这与函数嵌套一样允许您避免创建中间结果,但仍保持较高的可读性。
let squareOddValuesAndAddOnePipeline values =
values
|> List.filter isOdd
|> List.map square
|> List.map addOne printfn "processing %A through 'squareOddValuesAndAddOnePipeline' produces: %A" numbers (squareOddValuesAndAddOnePipeline numbers) /// 您可以继续精简 “squareOddValuesAndAddOnePipeline”函数,通过使用
/// Lamdba表达式移除第二个 “List.map”
///
/// 注意,Lamdba表达式中也同样用到管道运算符。
/// can be used for single values as well. This makes them very powerful for processing data.
let squareOddValuesAndAddOneShorterPipeline values =
values
|> List.filter isOdd
|> List.map(fun x -> x |> square |> addOne) printfn "processing %A through 'squareOddValuesAndAddOneShorterPipeline' produces: %A" numbers (squareOddValuesAndAddOneShorterPipeline numbers) /// 最后,您可以解决需要显示地采用值作为参数的问题,通过使用“>>”来编写两个核
/// 心操作:筛选出偶数,然后平方和加1。同样,Lamdba表达式“fun x-> ...”也不需
/// 要,因为x 只是在该范围中被定义,以便将其传入函数管线。因此,“>>”可以在这
/// 里使用。
///
/// “squareOddValuesAndAddOneComposition”的结果本身就是一个将整数列表作
/// 为其输入的函数。 如果您使用整数列表执行“squareOddValuesAndAddOneComposition”,
/// 则会注意到它与以前的函数相同。
///
/// 这是使用所谓的函数组合。 这是可能的,因为F#中的函数使用Partial Application,
///每个数据处理操作的输入和输出类型与我们使用的函数的签名相匹配。
let squareOddValuesAndAddOneComposition =
List.filter isOdd >> List.map (square >> addOne) printfn "processing %A through 'squareOddValuesAndAddOneComposition' produces: %A" numbers (squareOddValuesAndAddOneComposition numbers)

上述的示例使用了许多 F# 的特性,包括列表处理函数,头等函数和部分应用程序。虽然对于每个概念都有深刻的理解是较为困难的,但应该清楚的是,在使用函数管线来处理数据有多么容易。

6. 列表、数组和序列

列表、数组和序列是 F# 核心库中3个基础的集合类型。

列表是有序的、不可变的、具有相同类型元素的集合。它们是单链表,这意味着它们是易于枚举的,但是如果它们很大,则不易于随机存取和则随机访问和级联。这与其他流行语言中的列表不同,后者通常不使用单链表来表示列表。

 module Lists =

     /// 列使用“[...]”定义,这是一个空列表。
let list1 = [ ] /// 这是一个包含3个元素的列表, “;”用于分割在同一行的元素。
let list2 = [ ; ; ] /// 您也可以将各元素独占一行以进行分割。
let list3 = [ ] /// 这是一个包含1到1000整数的列表。
let numberList = [ .. ] /// 列表可以通过计算得到,这是包含一年中所有天的列表。
let daysList =
[ for month in .. do
for day in .. System.DateTime.DaysInMonth(, month) do
yield System.DateTime(, month, day) ] // 使用“List.take”输出“dayList”中的前5个元素。
printfn "The first 5 days of 2017 are: %A" (daysList |> List.take ) /// 计算中可以包含条件判断。 这是一个包含棋盘上的黑色方块的坐标元组的列表。
let blackSquares =
[ for i in .. do
for j in .. do
if (i+j) % = then
yield (i, j) ] /// 列表可以使用“List.map”和其他函数式编程组合器进行转换。 此处通过使用
/// 使用管道运算符将参数传递给List.map,计算列表中数字的平方,产生一个新的列表。
let squares =
numberList
|> List.map (fun x -> x*x) /// 还有很多其他列表组合器。如下计算能被3整除的数字的平方数。
let sumOfSquares =
numberList
|> List.filter (fun x -> x % = )
|> List.sumBy (fun x -> x * x) printfn "The sum of the squares of numbers up to 1000 that are divisible by 3 is: %d" sumOfSquares

数组是大小固定的、可变的、具有相同类型的元素的集合。 它们支持元素的快速随机访问,并且比F#列表更快,因为它们是连续的内存块。

 module Arrays =

     /// 这是一个空数组。注意,语法与列表相似,但是数组使用的是“[| ... |]”。
let array1 = [| |] /// 数组使用与列表相同的方式分割元素。
let array2 = [| "hello"; "world"; "and"; "hello"; "world"; "again" |] /// 这是一个包含1到1000整数的数组。
let array3 = [| .. |] /// 这是一个只包含“hello”和“world”的数组。
let array4 =
[| for word in array2 do
if word.Contains("l") then
yield word |] /// 这是一个由索引初始化的数组,其中包含从0到2000的偶数。
let evenNumbers = Array.init (fun n -> n * ) /// 使用切片符号提取子数组。
let evenNumbersSlice = evenNumbers.[..] /// 您可以使用“for”遍历数组和列表。
for word in array4 do
printfn "word: %s" word // 您可以使用左箭头分配运算符修改数组元素的内容。
array2.[] <- "WORLD!" /// 您可以使用“Array.map”和其他函数式编程操作来转换数组。
/// 以下计算以“h”开头的单词的长度之和。
let sumOfLengthsOfWords =
array2
|> Array.filter (fun x -> x.StartsWith "h")
|> Array.sumBy (fun x -> x.Length) printfn "The sum of the lengths of the words in Array 2 is: %d" sumOfLengthsOfWords

序列是一系列逻辑的元素,全部是相同的类型。 这些是比列表和数组更常用的类型,可以将其作为任何逻辑元素的“视图”。 它们脱颖而出,因为它们可以是惰性的,这意味着元素只有在需要时才被计算出来。

 module Sequences = 

     /// 这是一个空的队列。
let seq1 = Seq.empty /// 这是这是含有值的队列。
let seq2 = seq { yield "hello"; yield "world"; yield "and"; yield "hello"; yield "world"; yield "again" } /// 这是包含1到1000整数的队列。
let numbersSeq = seq { .. } /// 这是包含“hello”和“world”的队列。
let seq3 =
seq { for word in seq2 do
if word.Contains("l") then
yield word } /// 这个队列包含到2000范围内的偶数。
let evenNumbers = Seq.init (fun n -> n * ) let rnd = System.Random() /// 这是一个随机的无限序列。
/// 这个例子使用“yield!”返回子队列中的每个元素。
let rec randomWalk x =
seq { yield x
yield! randomWalk (x + rnd.NextDouble() - 0.5) } /// 这个例子显示了随机产生的前100个元素。
let first100ValuesOfRandomWalk =
randomWalk 5.0
|> Seq.truncate
|> Seq.toList printfn "First 100 elements of a random walk: %A" first100ValuesOfRandomWalk