Loading

学习笔记-涛讲F#(基础)

简介

F# 语言是面向 .NET 的多范例编程语言。 F# 支持函数式、命令式、面向对象的编程模式。
新建一个“Hello world”项目,代码如下:

// Learn more about F# at http://docs.microsoft.com/dotnet/fsharp
open System

// Define a function to construct a message to print
let from whom =
    sprintf "from %s" whom

[<EntryPoint>]
let main argv =
    let message = from "F#" // Call the function
    printfn "Hello world %s" message
    0 // return an integer exit code

注意以下几点:

  • open 导入声明:导入声明指定模块或命名空间,无需使用完全限定的名称即可引用其中的元素,参考 导入声明:open 关键字
  • let 绑定:绑定可将标识符与值或函数相关联,let 关键字用于将名称绑定到值或函数,参考 let 绑定
  • [<EntryPoint>]:显式指定main入口函数(显式入口点),没有显示指定入口点时代码会从第一行执行到最后一行(隐式入口点),参考 控制台应用程序

类型推导

F#会自动推导类型,也支持人工指定类型,代码如下:

//类型推导
let message = "F#" 

//指定类型
let (message:string) = "F#"

//指定错误类型-报错
let (message:int) = "F#"

多个输入参数的函数

函数有多个输入参数时,直接用空格分隔放在函数名后面,代码如下:

open System

//小孩票价三块,大人票价5块,求总价
let familyCost child adult =
    let result =child*3+adult*5
    result

[<EntryPoint>]
let main argv =
    let cost = familyCost 2 2 
    printfn $"total cost = {cost}"
    0 // return an integer exit code

注:如果参数添加括号,说明该组输入参数是元组。

定义单位

上面的familyCost函数调用时需要看函数定义才知道输入参数的含义,如果给每个参数定义一个单位代码就清晰多了,代码如下:

open System

//小孩票价三块,大人票价5块,求总价
let familyCost child adult =
    let result =child*3+adult*5
    result

[<Measure>]type 元
[<Measure>]type 小孩
[<Measure>]type 大人

let kidPrice=3<元/小孩>
let adultPrice=5<元/大人>

let familyCost2 (child:int<小孩>) (adult:int<大人>) =
    let result =child*kidPrice+adult*adultPrice
    result

[<EntryPoint>]
let main argv =
    let cost = familyCost 2 2 
    printfn $"total cost = {cost}"
    let cost2 = familyCost2 2<小孩> 2<大人>
    printfn $"total cost = {cost2}"
    0 // return an integer exit code

注:Measure 用于编译时单位检查,并不会生成在最终的代码中,不会影响性能也无法通过反射查看到

Measure 的定义可以参考 度量单位,它的语法如下:

[<Measure>] type unit-name [ = measure ]

分别定义两个单位和它们之间的转换关系,代码如下:

//定义度量值 cm(厘米)
[<Measure>] type cm
//将度量值 ml(毫升)定义为立方厘米 (cm^3)
[<Measure>] type ml = cm^3
  • 在涉及单位的公式中,支持整数幂(正和负)
  • 单位之间的空格指示两个单位的积,* 也指示单位的积,/ 指示单位的商
  • 对于倒数单位,可以使用负整数幂/(指示单位公式的分子与分母之间的分隔), 分母中的多个单位应括在括号中
  • / 后用空格分隔的单位解释为分母的一部分,但跟踪 * 后的任何单位都解释为分子的一部分,如单位公式 kg m s^-2 和 m /s s * kg 都会转换为 kg m/s^2
    两个单位涉及到数值转换的可以使用以下方式,代码如下:
//定义度量值 g(克)
[<Measure>] type g
//定义度量值 kg(千克)
[<Measure>] type kg
//定义转换常数,g kg^-1 与 g/kg 没有任何区别
let gramsPerKilogram : float<g kg^-1> = 1000.0<g/kg>
//定义转换函数
let convertGramsToKilograms (x : float<g>) = x / gramsPerKilogram

偏函数

偏函数是对原始函数的二次封装,将现有函数的部分参数预先绑定为指定值,从而得到一个新的函数,该函数就称为偏函数
偏函数比原函数具有更少的可变参数,降低了函数调用的难度。
柯里化与偏函数并不完全等同,两者的区别可以参考 函数柯里化与偏函数

一个简单的偏函数应用示例,代码如下:

let ask student ``a question`` = 
    printf "me ask %s: %s" student ``a question``

let askJohn =ask "John"

askJohn "How old are you?"

注意以下几点:

  • 代码没有显示指定入口点,程序会从第一行代码开始执行
  • F#变量命名支持空格,只需将变量用``括起来即可

常量也是函数

open System

//常量函数
let f1(x)=2
let f2 x=2
//常量
let f3 =2

返回值(unit与ignore)

每个 F# 表达式的计算结果必须为一个值,对于不生成相关值的表达式,使用 unit 类型的值
unit 类型类似于 C# 和 C++ 等语言中的 void 类型,参考 unit 类型
unit 类型具有单个值,该值由标记 () 指示,经常在 F# 编程中用于保存值是语言语法所必需的位置(但实际不需要任何值)。
因为 printf 操作的重要操作发生在函数中,所以函数不必返回实际值。 因此,printf 函数的返回值为 unit 类型,代码如下:

//返回值为unit
let print=printfn "check here %d"
print 3

//print2为unit类型值,内部会直接执行打印
let print2= print 4
//print2

//print3为函数,传入参数内部才会执行打印
let print3 _= print 5
print3 ()

关于unit类型的使用可以参考 F# 之 Unit 與 Ignore 簡介

如果函数返回的不是unit类型并且程序没有处理返回值,就需要使用ignore消除警告:

let sum x y = x + y 
sum 2 3 |> ignore

函数串联实现“开方乘十”

“开方”和“乘十”是两个操作,可以通过 函数正向组合运算符 >> 组合两个函数,执行先开方再乘十的操作,代码如下:

//函数重命名
let 开方=sqrt
//操作符重定义
let 乘十=(*)10.
//函数串联
let 开方乘十=开方>>乘十

//36后面必须加一个 . 符号表示类型为float,否则不能进行sqrt运算
let result=开方乘十 36.
printfn $"result={result}"

操作符重定义代码需要注意,运算符是用括号括起来的特殊名称的函数-参考 F#运算符重载。代码 let 乘十=(*)10. 相当于将基于加运算操作生成一个偏函数,所以后面的“10”替换的是加操作的前一个操作数。
一个简单的示例,代码如下:

//操作符重定义
let ``inc 1``=(+)1
let toKiloMeter=(*)1.6
let ``20 div``=(/)20

let result1=``inc 1`` 50
printfn $"result={result1}"
//打印结果:result=51

let result2=``20 div`` 4
printfn $"result={result2}"
//打印结果:result=5

使用管道符 |>

管道符 |>是F#内部实现的一个符号,其作用是将左侧表达式的结果传递给右侧的函数,实现代码其实很简单:

let inline (|>) x f = f x

继续使用“开方乘十”的示例,代码如下:

let 开方=sqrt
let 乘十=(*)10.
let 开方乘十=开方>>乘十

let 学生分数=60.0
let result=开方乘十 学生分数
printfn $"result={result}"

使用管道符可以让代码读写变得更轻松:

//原代码
//let result=开方乘十 学生分数
//使用管道符的代码
let result=学生分数 |> 开方乘十 

也可以把“开方乘十”函数展开:

let result=学生分数 |> 开方 |> 乘十 

格式化一下代码会更容易阅读:

let result=
    学生分数 
    |>开方
    |>乘十

元组(参数加上括号)

元组是一组未命名但有序的值,值的类型可能不同。 元组可以是引用类型或结构,具体定义参考 元组
元组语法如下:

//引用元组
(element, ... , element)
//结构元组
struct(element, ... ,element )

注:上述语法中的每个 element 都可以是任何有效的 F# 表达式

一个应用元组计算两点距离的示例:

let distance(x0,y0)(x1,y1)=sqrt((x0-x1)**2. + (y0-y1)**2.)

//不加括号定义元组
let a=2. ,2.0
//加括号定义元组
let original=(0.,0.)

let x=a |>distance original

printfn "%A" x

一个元组解构取值的示例:

let tuple=(1,2,3)
let b=(2,3,5)
let c=(tuple,12,13)

let (x,_,z)= tuple
let a,_,_=c 
let(x0,y0,z0),_,_=c 

printfn $"{x} and {z}"
printfn $"{a} and {x0}"

注:用_表示不关注的值,否则需要为所有不关注的值取一个不重复的名称。

元组只适合内部运算使用,不要将元组用于外部交互,这里的“外部”包括用户、同事甚至一段时间后的自己。

F#中的类

F#作为函数式编程语言,提供类的定义主要基于以下两方面考虑:

  • 与其它面向对象语言交互
  • 作为一门成熟的语言并不排斥面向对象中一些成熟的概念,比如类

在F#中类只是组织一些数据、一些方法的组织结构,定义类只需要使用type关键字即可。

一个定义狗的类,代码如下:

type Dog()=
    member val Age=2 with get,set
    member val Breed="狼狗" with get,set

    member this.叫()="汪汪"
    member this.哭()=printfn "呜呜"

let dog=Dog()
dog.叫()|>printfn "%s"
dog.哭()

F#中的类也支持构造函数、继承、虚方法等面向对象中的概念,有兴趣的可以看看 类 (F#)

记录

记录表示已命名值的简单聚合,可选择包含成员。 记录可以是结构或引用类型, 默认情况下是引用类型,示例如下:

//在同一行上定义标签时,标签用分号分隔
type Point = { X: float; Y: float; Z: float; }

//您可以在自己的行上定义标签,可以使用分号,也可以不使用分号
type Customer = 
    { First: string
      Last: string;
      SSN: uint32
      AccountNumber: uint32; }

//结构记录
[<Struct>]
type StructPoint = 
    { X: float
      Y: float
      Z: float }

注:记录就是带了名字的元组,值会按照记录的域名自动匹配记录类型。如果遇到域名完全一样的记录,值的类型会匹配到最后定义的那个记录类型。

复制和更新记录表达式

复制和更新记录表达式是一个复制现有记录更新指定字段并返回更新后的记录的表达式。

let myRecord2 = { MyRecord.X = 1; MyRecord.Y = 2; MyRecord.Z = 3 }
let myRecord3 = { myRecord2 with Y = 100; Z = 2 }

元组、记录、类的对比

一个简单的对比示例:

let dog0=("Tao","German shepherd","Lucky",3)

//匿名记录
let dog={|Owner="Tao";Breed="German shepherd";DogName="Lucky";Age=3;|}
    
type Dog()=
    member val Owner="Tao" with get
    member val Breed="German shepherd" with get
    member val DogNmae="Lucky" with get
    member val Age=3
let dog2=Dog()

三者优缺点如下:

  • 元组:语法简单、方便,适用于函数内部或一些示例,不能用于外部交互
  • 记录:语法适中、结构清晰,大多数情况都可以使用记录
  • 类:语法略复杂,使用代价比记录略大,一般用的较少

总的来说,什么时候使用元组、记录、类要自己根据具体的场景来判断。

联合

F#中的联合类似于其他语言中的联合类型,但存在一些差异,具体区别参考 可区分联合
一个简单的联合示例:

//Union,DU,or 联合曰type Point=
type Point=
    |TwoD of int*int
    |ThreeD of int*int*int 
    |OneD of int 

let p1=OneD(3);
let p2=TwoD(1,2)
let p3=ThreeD(2,3,5)
let printPoint (p:Point)=
    printfn "%A" p 

printPoint p2
printPoint p3

//打印结果
//TwoD (1, 2)
//ThreeD (2, 3, 5)

使用联合做统一操作

在面向对象中我们一般通过基类对不同类型的对象做统一操作,这种方法需要更改代码使操作对象继承同一个基类。
F#可以使用联合来实现对不同类型的对象做统一操作,并且不需要定义基类,代码如下:

//Uni//Union,DU,or 联合
type Dog={Owner:string;Breed:string;DogName:string;Age:int}
let dog ={Owner="Tao";Breed="corgi";DogName="Lucky";Age=2;}
type Cat()=
    member val Owner="Tao"with get,set 
    member val Name="Lovely"with get,set 
    member val Age=2 with get,set     
let cat=new Cat()

//定义联合
type Animal=
    |狗 of Dog
    |猫 of Cat

let printName animal=
    //模式匹配
    match animal with
    |狗 d->printfn "%A" d.DogName
    |猫 c->printfn "%A" c.Name

let d=狗 dog 
let c=猫 cat 

printName d
printName c

//打印结果
//"Lucky"
//"Lovely"

注:match 表达式提供基于表达式与一组模式的比较结果的分支控制,详细说明参考 模式匹配

使用联合进行分类

联合不仅可以进行基类操作,还可以进行分类,代码如下:

type Category=
    | Zero
    | Small of int
    | Big
    | Huge
    | VeryHuge

let categorize x=
    match x with
    | 0 -> Zero
    | 1
    | 2 -> Small(x)
    | _ when x>2 && x<10 -> Big
    | _ when x>=10 && x<100 -> Huge
    | _ ->VeryHuge

categorize 2 
//交互执行结果:val it : Category = Small 2

实现树数据结构

联合可以是递归的,可将联合本身包含在一个或多个用例的类型中用于创建树结构,这些结构用于在编程语言中对表达式建模。
一个二叉树数据结构示例:

type Tree =
    | Tip
    | Node of int * Tree * Tree

let rec sumTree tree =
    match tree with
    | Tip -> 0
    | Node(value, left, right) ->
        value + sumTree(left) + sumTree(right)
let myTree = Node(0, Node(1, Node(2, Tip, Tip), Node(3, Tip, Tip)), Node(4, Tip, Tip))
let resultSumTree = sumTree myTree
// resultSumTree值为10

联合包括两个用例,即 Node(具有整数值以及左子树和右子树的节点)和 Tip(用于终止树),树的结构如下:

IF和多分支

F#中的if和其它语言类似,只是必须带else,否则报错。F#中的分支控制是由match负责,没有switch关键字。
示例如下:

//if必须带else
let f a=if a%2=0 then "even"else "odd"

//模式匹配(类似switch分支)
let f2 a=
    match a with
    | 0 -> "Zero"
    | 1 -> "One"
    | 2 -> "Two"
    //当a大于2且小于100时
    | _ when a > 2 && a < 180 -> "big number"
    //当a大于等于100时
    | _ when a >= 100 -> "huge number"
    //抛出异常 "I do not understand this number"
    | _ -> failwith "I do not understand this number"

注:failwith 函数会生成 F# 异常,用法参考 异常:failwith 函数

不可变的变量

F# 认为不可变值最为重要,而不是可在程序执行过程中赋予新值的变量, 不可变数据是函数编程中的一个重要元素,参考
如果过程语言中的代码使用变量赋值来更改值,函数语言中的等效代码会有一个作为输入的不可变值、一个不可变函数以及作为输出的其他不可变值。

//immutable 不可变变量
let a=2
//mutable 可变变量
let mutable b=3
//将新值赋给可变变量
b<-5

//交换值
let swap(a,b)=b,a  
let x=swap(2,4)

不使用NULL(Some和None)

F#中有NULL值,主要用来接受其它语言的NULL值,内部是不会使用NULL值的,参考 Null 值
F#使用Some将数据分为:

  • 可用(Some):符合函数预期的合法值
  • 不可用(None):不符合函数预期的非法值(包括Null)

SomeNone属于 Option 联合类型的用例,Option 类型是 F# 核心库中的简单可区分联合,声明方式如下:

type Option<'a> =
    | Some of 'a
    | None

一个判定数据合法、非法的示例:

let a=2

//只有0是合法值
let makeOption x=
    if x=0 then None
    else Some(x)

let f b=
    match b with
    | Some(_)->"average value"
    | None->"odd point"

a |> makeOption |> f
posted @ 2022-08-18 23:00  二次元攻城狮  阅读(736)  评论(0编辑  收藏  举报