Y分钟速成X

其中 X=F#

F# 是一款通用的、函数式的面向对象语言。它既开源又免费,并且在Linux、Mac和Windows等多平台上均可运行。

这款语言拥有强大的类型系统,许多错误在编译期间即可被发现并随之被修复。不过,由于 F# 采用了类型推理技术(译者注:不需手动指定,而是通过上下文自动推断变量的类型),在读代码时往往给人一种动态类型语言的错觉。

需要留意的是, F# 的语法和“类C语言”有较大不同:

若您想尝试运行以下代码,看看效果如何的话,请移步https://try.fsharp.org,将代码粘贴进交互式REPL界面运行吧。

// 单行注释以双斜杠开头
(* 多行注释以包裹在 (* , *) 之间
多行注释至此结束- *)

// ================================================
// 基础语法
// ================================================

// ------ "变量" (实际上默认不可变) ------
// "let" 关键字可定义一个(不可变的)变量
let myInt = 5
let myFloat = 3.14
let myString = "hello" // 注意,并不需要指定类型

// 可变变量用 "mutable" 标注
let mutable a=3
a <- 4 // a现在的值是4

// 稍有不同的可变变量
// Reference cell是一种容器,允许您使用引用语义来创建可变变量(create mutable values with reference semantics)
// 译者注:和Rust的RefCell类似,提供容器内部的可变性
// 详见 https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/reference-cells
let xRef = ref 10
printfn "%d" xRef.Value // 10
xRef.Value <- 11
printfn "%d" xRef.Value // 11

let a = [ref 0; ref 1] // 装有可变元素的列表
a[0].Value <- 2

// ------ 列表Lists ------
let twoToFive = [2; 3; 4; 5]     // 使用方括号创建列表
                                 // 元素间使用**分号**分隔
let oneToFive = 1 :: twoToFive   // :: 创建新列表,并将新元素添加到开头
// 结果为 [1; 2; 3; 4; 5]
let zeroToFive = [0; 1] @ twoToFive   // @ 可合并两个列表

// 重要提示:是使用分号来分隔语句,而不是逗号!!

// ------ 函数 ------
// "let" 关键字也可以用来定义函数
let square x = x * x          // 注意,没有使用圆括号
square 3                      // 注意,运行函数时也不需要圆括号

let add x y = x + y           // 切勿将参数写成 (x, y)!
                              // 这是元组Tuple的写法(稍后介绍),
                              // 和函数参数完全不同
add 2 3                       // 调用该函数

// 使用缩进来定义一个多行的复杂函数。不需要写分号
let evens list =
   let isEven x = x % 2 = 0   // 定义**子函数** "isEven"。
                              // 注意“相等”是使用单等号'='而不是双等号"=="
   List.filter isEven list    // List.filter 是个 F# 库函数,它有两个参数:
                              // - 返回值为boolean的函数
                              // - 待处理的列表

evens oneToFive               // 调用该函数

// 圆括号可用于显式地标注计算优先级。在下述示例代码中:
// 1. 首先进行 List.map 计算,该函数具有两个参数
// 2. 然后进行 List.sum 计算,将上一步的结果作为该步的参数
// 若不写圆括号,"List.map" 就会被当作参数传递给 List.sum

let sumOfSquaresTo100 =
   List.sum ( List.map square [1..100] )

// 使用管道操作符 "|>" ,可以将上一步的输出(译者注:即返回值)输送给下一步作为输入
// 管道操作在 F# 中很常见,其语义也与 UNIX 操作系统中的管道操作非常相似。

// 使用管道操作符,可将 sumOfSquares 函数重构为如下形式:
let sumOfSquaresTo100piped =
   [1..100] |> List.map square |> List.sum  // "square" 函数就是之前定义的,求平方的函数

// 匿名函数(或作lambda函数)可用 "fun" 关键字定义
let sumOfSquaresTo100withFun =
   [1..100] |> List.map (fun x -> x * x) |> List.sum

// F# 中没有"return" 关键字,因为函数总是返回最后一个表达式的值。

// ------ 模式匹配 ------
// 模式匹配 match..with.. 是个加强版的 case/switch 
let simplePatternMatch =
   let x = "a"
   match x with
    | "a" -> printfn "x is a"
    | "b" -> printfn "x is b"
    | _ -> printfn "x is something else"   // 下划线可匹配任意值

// F# 默认不支持空值(null),您必须使用Option类型以便随后进行模式匹配
// Some(..) 和 None 均为Option类型的一种,用于表示"可能为空值"的两种情况
let validValue = Some(99)
let invalidValue = None

// 在下列示例中,match..with 不仅匹配了 "Some" 和 "None" 两种情况,
// 还同时将 "Some" 中的内容给解包出来了
let optionPatternMatch input =
   match input with
    | Some i -> printfn "input is an int=%d" i
    | None -> printfn "input is missing"

optionPatternMatch validValue
optionPatternMatch invalidValue

// ------ 打印内容 ------
// printf/printfn 函数与 C# 中的 Console.Write/WriteLine 函数类似
printfn "Printing an int %i, a float %f, a bool %b" 1 2.0 true
printfn "A string %s, and something generic %A" "hello" [1; 2; 3; 4]

// 若要按一定格式生成字符串,可以使用 sprintf/sprintfn 函数,
// 它们与 C# 中的 String.Format 函数类似

// ================================================
// 函数漫谈
// ================================================

// F# 是函数式编程语言:函数是其一等公民,通过组合不同函数可以实现强大的功能

// 模块(Modules)可以将一系列函数组织到一起
// 每个模块使用标识符(Indentation)来唯一标记
module FunctionExamples =

    // 定义一个简单的求和函数
    let add x y = x + y

    // 函数的基础使用
    let a = add 1 2
    printfn "1 + 2 = %i" a

    // 函数的“部分应用”(partial application)可事先“固化”参数
    let add42 = add 42
    let b = add42 1
    printfn "42 + 1 = %i" b

    // 函数的组合(composition)可将多个函数“串接”起来
    let add1 = add 1
    let add2 = add 2
    let add3 = add1 >> add2
    let c = add3 7
    printfn "3 + 7 = %i" c

    // 高阶函数
    [1..10] |> List.map add3 |> printfn "new list is %A"

    // 函数列表,以及 List.reduce 函数
    let add6 = [add1; add2; add3] |> List.reduce (>>)
    let d = add6 7
    printfn "1 + 2 + 3 + 7 = %i" d

// ================================================
// 列表与集合(collection)
// ================================================

// F# 内置了三种有序集合:
// * 列表(List)是最基本的、不可变的集合类型
// * 数组(Arrays)是可变的,按需使用可令程序更高效
// * 序列(Sequences)是惰性的、无限的集合类型(例如枚举器enumerator)
//
// 除此以外,还有诸如不可变哈希表、不可变哈希集合、
// 以及所有 .NET 中定义的标准集合类型等

module ListExamples =

    // 列表使用方括号
    let list1 = ["a"; "b"]
    let list2 = "c" :: list1    // :: 将元素添加到开头
    let list3 = list1 @ list2   // @ 将两个列表连接起来

    // 列表推导式(list comprehensions),也被称为生成器(generators)
    let squares = [for i in 1..10 do yield i * i]

    // 素数生成器
    // - 本例采用了模式匹配的简写语法
    // - (p::xs) 匹配列表的 “第一个元素 :: 其余所有元素” ,写成 p :: xs也可以
    //   此时,p匹配到列表的第一个元素,而其余元素被xs匹配到
    //   这种写法被称为构造模式(cons pattern)
    // - 在写递归函数时,必须使用 "rec" 关键字
    let rec sieve = function
        | (p::xs) -> p :: sieve [ for x in xs do if x % p > 0 then yield x ]
        | []      -> []
    let primes = sieve [2..50]
    printfn "%A" primes

    // 对列表进行模式匹配
    let listMatcher aList =
        match aList with
        | [] -> printfn "the list is empty"
        | [first] -> printfn "the list has one element %A " first
        | [first; second] -> printfn "list is %A and %A" first second
        | first :: _ -> printfn "the list has more than two elements, first element %A" first

    listMatcher [1; 2; 3; 4]
    listMatcher [1; 2]
    listMatcher [1]
    listMatcher []

    // 接受列表作参数的递归函数
    let rec sum aList =
        match aList with
        | [] -> 0
        | x::xs -> x + sum xs
    sum [1..10]

    // -----------------------------------------
    // 列表的标准库函数
    // -----------------------------------------

    // map
    let add3 x = x + 3
    [1..10] |> List.map add3

    // filter
    let even x = x % 2 = 0
    [1..10] |> List.filter even

    // 还有很多,详见文档

module ArrayExamples =

    // 数组(Array)用 [| 和 |] 包裹
    let array1 = [| "a"; "b" |]
    let first = array1.[0]        // 用 . 来索引元素

    // 数组和列表的模式匹配语法完全相同
    let arrayMatcher aList =
        match aList with
        | [| |] -> printfn "the array is empty"
        | [| first |] -> printfn "the array has one element %A " first
        | [| first; second |] -> printfn "array is %A and %A" first second
        | _ -> printfn "the array has more than two elements"

    arrayMatcher [| 1; 2; 3; 4 |]

    // Array的标准库函数和List也几乎一样

    [| 1..10 |]
    |> Array.map (fun i -> i + 3)
    |> Array.filter (fun i -> i % 2 = 0)
    |> Array.iter (printfn "value is %i. ")


module SequenceExamples =

    // 序列(Sequence)用大括号包裹
    let seq1 = seq { yield "a"; yield "b" }

    // 序列可以使用 "yield" 关键字,也可以包含子序列
    let strange = seq {
        // "yield" 向序列中增添1个元素
        yield 1; yield 2;

        // "yield!" 则是增添一个子序列
        yield! [5..10]
        yield! seq {
            for i in 1..10 do
              if i % 2 = 0 then yield i }}
    // 试试看吧!
    strange |> Seq.toList


    // 序列可以通过 "Seq.unfold" 函数来创建
    // 下例演示如何用这个函数创建斐波那契数列
    let fib = Seq.unfold (fun (fst,snd) ->
        Some(fst + snd, (snd, fst + snd))) (0,1)

    // 试试看吧!
    let fib10 = fib |> Seq.take 10 |> Seq.toList
    printf "first 10 fibs are %A" fib10


// ================================================
// 数据类型
// ================================================

module DataTypeExamples =

    // 默认情况下,所有的数据类型均是不可变的

    // 元组(Tuple)是一种简单快捷的匿名类型
    // -- 使用逗号即可创建元组
    let twoTuple = 1, 2
    let threeTuple = "a", 2, true

    // 同样,使用模式匹配来解包元组
    let x, y = twoTuple  // x, y分别被赋值为1, 2

    // ------------------------------------
    // 记录(Record)类型由命名域构成(译者注:类似class的成员变量)
    // 译者注:由于“记录”一词听起来像动词,下文将以英文原文Record来指代
    // ------------------------------------

    // 使用 "type" 关键字和大括号来定义Record类型
    type Person = {First:string; Last:string}

    // 使用 "let" 关键字和大括号来创建Record实例
    let person1 = {First="John"; Last="Doe"}

    // 同样,使用模式匹配来解包Record实例
    let {First = first} = person1    // first被赋值为"John"

    // ------------------------------------
    // 联合类型(Union或Variants,类似于Rust中的枚举类型)拥有一系列的取值选项。其实例仅能从中取其一。
    // 译者注:由于联合类型与Rust中的枚举类型(enum)很相似,故后文将以“枚举类型”指代之
    // ------------------------------------

    // 使用 "type" 关键字和竖线/管道符来定义枚举类型
    type Temp =
        | DegreesC of float
        | DegreesF of float

    // 使用一个选项来创建枚举实例
    let temp1 = DegreesF 98.6
    let temp2 = DegreesC 37.0

    // 模式匹配可以解包枚举实例
    let printTemp = function
       | DegreesC t -> printfn "%f degC" t
       | DegreesF t -> printfn "%f degF" t

    printTemp temp1
    printTemp temp2

    // ------------------------------------
    // 递归类型
    // ------------------------------------

    // 类型可以通过递归组合成复杂的类型,无需创建子类
    type Employee =
      | Worker of Person
      | Manager of Employee list // 译者注:这儿发生了递归定义

    let jdoe = {First="John"; Last="Doe"}
    let worker = Worker jdoe

    // ------------------------------------
    // 使用枚举类型建模
    // ------------------------------------

    // 枚举类型非常适合用于表述某种状态,再也不需要用数字等标志位(flag)来表征状态啦!
    type EmailAddress =
        | ValidEmailAddress of string   // 状态:合法邮件地址
        | InvalidEmailAddress of string // 状态:不合法邮件地址

    let trySendEmail email =
        match email with // 使用模式匹配
        | ValidEmailAddress address -> ()   // 可以发送
        | InvalidEmailAddress address -> () // 不能发送

    // 组合使用枚举类型和Record类型为“域驱动的软件设计”(Domain Driven Design)提供了良好基础。
    // 您可以定义数以百计的类型,每一种都精准地反映着一个域(Domain)

    type CartItem = { ProductCode: string; Qty: int }
    type Payment = Payment of float
    type ActiveCartData = { UnpaidItems: CartItem list }
    type PaidCartData = { PaidItems: CartItem list; Payment: Payment}

    type ShoppingCart =
        | EmptyCart  // 空购物车,没有数据
        | ActiveCart of ActiveCartData
        | PaidCart of PaidCartData

    // ------------------------------------
    // 数据类型的内置行为
    // ------------------------------------

    // 核心数据类型提供了开箱即用的默认行为与性质,无需额外编码。
    // * 不可变性
    // * 漂亮的打印输出,在debug时尤其好用
    // * 相等性与可比性
    // * 可序列化性

    // 使用 %A 来输出复杂数据类型
    printfn "twoTuple=%A,\nPerson=%A,\nTemp=%A,\nEmployee=%A"
             twoTuple person1 temp1 worker

    // 下列扑克牌示例展示了 F# 中内置的相等性与可比较性
    type Suit = Club | Diamond | Spade | Heart
    type Rank = Two | Three | Four | Five | Six | Seven | Eight
                | Nine | Ten | Jack | Queen | King | Ace

    let hand = [ Club, Ace; Heart, Three; Heart, Ace;
                 Spade, Jack; Diamond, Two; Diamond, Ace ]

    // 排序
    List.sort hand |> printfn "sorted hand is (low to high) %A"
    List.max hand |> printfn "high card is %A"
    List.min hand |> printfn "low card is %A"


// ================================================
// 主动模式(Active patterns)
// ================================================

module ActivePatternExamples =

    // F# 中,有一种被称为“主动模式”的特殊模式匹配
    // 它可以在模式匹配中动态解析或检测模式(pattern)。
    // 译者注:译者个人倾向于把此所谓“主动模式”理解为,自定义某个数据类型在接受模式匹配时,会返回哪些情况

    // 主动匹配的语法形似 "banana clips" (译者注:确实不知道banana clips是啥意思)

    // 您可以用elif代替else if,它们在 F# 中完全等价

    // 下列示例使用主动模式去匹配不同类型的字符...
    let (|Digit|Letter|Whitespace|Other|) ch =
       if System.Char.IsDigit(ch) then Digit
       elif System.Char.IsLetter(ch) then Letter
       elif System.Char.IsWhiteSpace(ch) then Whitespace
       else Other

    // ...然后使用它,可以看到解析逻辑十分清晰
    let printChar ch =
      match ch with
      | Digit -> printfn "%c is a Digit" ch
      | Letter -> printfn "%c is a Letter" ch
      | Whitespace -> printfn "%c is a Whitespace" ch
      | _ -> printfn "%c is something else" ch

    // 用主动模式处理并打印一个列表
    ['a'; 'b'; '1'; ' '; '-'; 'c'] |> List.iter printChar

    // -----------------------------------
    // 用主动模式实现FizzBuzz
    // -----------------------------------

    // 您也可以在主动模式中实现“部分匹配”
    // 只需在定义中使用下划线,并在匹配成功时返回 Some 即可
    let (|MultOf3|_|) i = if i % 3 = 0 then Some MultOf3 else None
    let (|MultOf5|_|) i = if i % 5 = 0 then Some MultOf5 else None

    // fizzbuzz的主函数
    let fizzBuzz i =
      match i with
      | MultOf3 & MultOf5 -> printf "FizzBuzz, "
      | MultOf3 -> printf "Fizz, "
      | MultOf5 -> printf "Buzz, "
      | _ -> printf "%i, " i

    // 试试看吧!
    [1..20] |> List.iter fizzBuzz

// ================================================
// 简明的 F#
// ================================================

module AlgorithmExamples =

    // F# 代码的“信噪比”很高,阅读代码时很容易就能弄明白算法的意图

    // ------ 例子: 计算平方和 ------
    let sumOfSquares n =
       [1..n]              // 1) 取从 1 到 n 的所有整数
       |> List.map square  // 2) 给每个整数求平方
       |> List.sum         // 3) 将上一步的结果求和

    // 试试看吧!
    sumOfSquares 100 |> printfn "Sum of squares = %A"

    // ------ 例子: 排序 ------
    // 译者注:下列示例实现的是朴素的快速排序算法
    let rec sort list =
       match list with
       // 若 list 是空表...
       | [] ->
            []                            // ...则返回空表
       // 否则...
       | firstElem::otherElements ->      // 取其第一个元素
            let smallerElements =         // 从剩余元素中提取比它小的
                otherElements
                |> List.filter (fun e -> e < firstElem)
                |> sort                   // 并排序
            let largerElements =          // 同理,从剩余元素中提取比它大的
                otherElements
                |> List.filter (fun e -> e >= firstElem)
                |> sort                   // 并排序
            // 最后,将这三部分组合起来,返回一个新的列表
            List.concat [smallerElements; [firstElem]; largerElements]

    // 试试看吧!
    sort [1; 5; 23; 18; 9; 1; 3] |> printfn "Sorted = %A"

// ================================================
// 异步编程
// ================================================

module AsyncExample =

    // F# 内置了对异步编程的支持
    // 从而规避了缩进地狱问题("pyramid of doom")
    // 下列示例展示了如何使用异步编程,实现同时下载多个网页

    open System.Net
    open System
    open System.IO
    open Microsoft.FSharp.Control.CommonExtensions

    // 异步地访问URL,并获取其内容
    let fetchUrlAsync url =
        async {   // "async" 关键词和大括号将创建一个异步对象 ("async" object)
            let req = WebRequest.Create(Uri(url))
            use! resp = req.AsyncGetResponse()
                // use! 的意思是异步赋值,类似于JavaScript的await
            use stream = resp.GetResponseStream()
                // "use" 会让资源在当前作用域结束时自动 close()
            use reader = new IO.StreamReader(stream)
            let html = reader.ReadToEnd()
            printfn "finished downloading %s" url
        }

    // 准备以下网页喂给爬虫
    let sites = ["http://www.bing.com";
                 "http://www.google.com";
                 "http://www.microsoft.com";
                 "http://www.amazon.com";
                 "http://www.yahoo.com"]

    // 来试试看吧!
    sites
    |> List.map fetchUrlAsync  // 把每个URL都包装成一个异步任务
    |> Async.Parallel          // 令所有任务并发地运行
    |> Async.RunSynchronously  // 开始运行

// ================================================
// .NET 兼容性
// ================================================

module NetCompatibilityExamples =

    // C#能干的活,F# 基本上都能做。同时,F# 还无缝集成了 .Net 或 Mono 的库
    // 译者注:Mono库(Mono Libraries)的含义不太确定

    // ------- 使用已有的库函数  -------

    let (i1success, i1) = System.Int32.TryParse("123");
    if i1success then printfn "parsed as %i" i1 else printfn "parse failed"

    // ------- 简简单单实现接口(interface) -------

    // 创建一个实现了 IDisposable 的对象
    let makeResource name =
       { new System.IDisposable
         with member this.Dispose() = printfn "%s disposed" name }

    let useAndDisposeResources =
        use r1 = makeResource "first resource"
        printfn "using first resource"
        for i in [1..3] do
            let resourceName = sprintf "\tinner resource %d" i
            use temp = makeResource resourceName
            printfn "\tdo something with %s" resourceName
        use r2 = makeResource "second resource"
        printfn "using second resource"
        printfn "done."

    // ------- 面向对象编程 -------

    // F# 对面向对象编程的支持也很成熟,支持类、接口、虚函数等

    // 带有泛型的接口
    type IEnumerator<'a> =
        abstract member Current : 'a
        abstract MoveNext : unit -> bool

    // 带有虚函数的抽象基类
    [<AbstractClass>]
    type Shape() =
        // 只读的成员变量
        abstract member Width : int with get
        abstract member Height : int with get
        // 非虚函数
        member this.BoundingArea = this.Height * this.Width
        // 虚函数,且带有默认实现
        abstract member Print : unit -> unit
        default this.Print () = printfn "I'm a shape"

    // 具象子类,继承上述抽象基类,并覆写(override)了其中的函数
    type Rectangle(x:int, y:int) =
        inherit Shape()
        override this.Width = x
        override this.Height = y
        override this.Print ()  = printfn "I'm a Rectangle"

    // 试试看吧!
    let r = Rectangle(2, 3)
    printfn "The width is %i" r.Width
    printfn "The area is %i" r.BoundingArea
    r.Print()

    // ------- 扩展已有类的方法  -------

    // 与 C# 一样,F# 也可以使用扩展语法来扩展已有类的方法
    type System.String with
       member this.StartsWithA = this.StartsWith "A"

    // 试试看吧!
    let s = "Alice"
    printfn "'%s' starts with an 'A' = %A" s s.StartsWithA

    // ------- 事件  -------

    type MyButton() =
        let clickEvent = new Event<_>()

        [<CLIEvent>]
        member this.OnClick = clickEvent.Publish

        member this.TestEvent(arg) =
            clickEvent.Trigger(this, arg)

    // 测试效果
    let myButton = new MyButton()
    myButton.OnClick.Add(fun (sender, arg) ->
            printfn "Click event with arg=%O" arg)

    myButton.TestEvent("Hello World!")

更多信息

欲参阅更多 F# 示例代码,请移步 why use F# 系列文章。

欲更深入了解 F# ,请移步 fsharp.orgdotnet's F# page


有建议?或者发现什么错误?在GitHub上开一个issue,或者发起pull request

原著Scott Wlaschin,并由1个好心人修改。